Home > Enterprise >  How to impose type constraints on typeclass instances?
How to impose type constraints on typeclass instances?

Time:07-31

I'm learning Haskell by reading Learn You a Haskell for Great Good!. Near the end of the Making Our Own Types and Typeclasses section, a class YesNo is defined to simulate truthiness in languages like javascript:

class YesNo a where  
    yesno :: a -> Bool 

instance YesNo Int where  
    yesno 0 = False  
    yesno _ = True  

(etc.)

I was trying to flesh out the instances myself as an exercise before reading the reference, and thought I could be clever and define it for all Num types:

instance (Num a) => YesNo a where
  yesno 0 = False
  yesno _ = True

I'll skip over how this requires FlexibleInstances, which I think I've understood between the docs and this answer. Once that's turned on, the compiler complains "The constraint 'Num a' is no smaller than the instance head 'YesNo a'". The answers to this question do a good job of explaining what that means. Using the newtype solution provided there, I come up with something like

newtype TruthyNum a = TruthyNum a

instance (Num a, Eq a) => YesNo (TruthyNum a) where
  yesno (TruthyNum 0) = False
  yesno _ = True

But now I have to say e.g. yesno $ TruthyNum 0 instead of yesno 0.

This doesn't feel right. Is there really no way to cleanly express yesno for Num types without writing out an instance for each such type? Or, taking a step back, how would an experienced Haskell hacker come at the premise of "define a typeclass that implements truthiness in the vein of [pick your scripting language]"?

CodePudding user response:

Very good question! I would exactly define a newtype like you did. I would not use it directly but deriving via it.

{-# Language DerivingVia              #-}
{-# Language StandaloneDeriving       #-}
{-# Language StandaloneKindSignatures #-}

import Data.Kind (Type, Constraint)

type  YesNo :: Type -> Constraint
class YesNo a where
  yesno :: a -> Bool

type    TruthyNum :: Type -> Type
newtype TruthyNum a = TruthyNum a

instance (Num a, Eq a) => YesNo (TruthyNum a) where
  yesno (TruthyNum 0) = False
  yesno _             = True

-- standalone deriving, is used when deriving an instance
-- outside of the data declaration
deriving via TruthyNum Int
  instance YesNo Int
deriving via TruthyNum Integer
  instance YesNo Integer
deriving via TruthyNum Float
  instance YesNo Float

Applicative lifting is another behavour that overlaps in this way. Given Applicative f you can lift algebras like

  • Semigroup a, Monoid a, Num a

into

  • Semigroup (f a), Monoid (f a), Num (f a)
instance (Applicative f, Num a) => Num (f a) where
  ( )         = liftA2 ( )
  (-)         = liftA2 (-)
  (*)         = liftA2 (*)
  negate      = liftA  . negate
  abs         = liftA  . abs
  signum      = liftA  . signum
  fromInteger = liftA0 . fromInteger where liftA0 = pure

Instead of writing an overlapping instance we make an instance of the newtype Ap f a.

type    Ap :: (k -> Type) -> (k -> Type)
newtype Ap f a = Ap (f a)
  deriving newtype (Functor, Applicative, ..)

instance (Applicative f, Num a) => Num (Ap @Type f a) where
  ( )         = liftA2 ( )
  (-)         = liftA2 (-)
  (*)         = liftA2 (*)
  negate      = liftA  . negate
  abs         = liftA  . abs
  signum      = liftA  . signum
  fromInteger = liftA0 . fromInteger where liftA0 = pure

Here is a trampoline example, that derives instances that depend on the previous derivation. The datatype is a "3D vector", but it's simply a datatype that has 3 arguments of the same type.

  1. Hardwired derivation of equality and a generic representation for V3
  2. Generically1 (a newtype) to derive Applicative using that representation
  3. Ap to derive Num by lifting over that Applicative
  4. TruthyNum to derive YesNo using equality and that Num instance

The Applicative we derive involves lifting pure a = V3 a a a so when we write 0 :: V3 Int we actually mean V3 0 0 0 :: V3 Int.

This means that your YesNo instance is implemented by comparing against (/= V3 0 0 0). So we treat V3 as "false" when the values are 0.

-- >> 0 :: V3 Int
-- V3 0 0 0
-- >> yesno (V3 0 0 0)
-- False
-- >> yesno (V3 0 0 2)
-- True
data V3 a = V3 a a a
  deriving
  stock (Eq, Show, Generic1)

  deriving (Functor, Applicative)
  via Generically1 V3

  deriving (Semigroup, Monoid, Num)
  via Ap V3 a

  deriving YesNo
  via TruthyNum (V3 a)

  • Related