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.
- Hardwired derivation of equality and a generic representation for
V3
Generically1
(anewtype
) to deriveApplicative
using that representationAp
to deriveNum
by lifting over thatApplicative
TruthyNum
to deriveYesNo
using equality and thatNum
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)