'Applicative for a user defined type

I'm trying to write Applicative for this type

data Choice a = ColumnA a | ColumnB a

I wrote a Functor instance:

instance Functor Choice where 
  fmap f (ColumnA a ) = (ColumnA (f a) )
  fmap f (ColumnB a ) = (ColumnB (f a) ) 

Now I want to write Applicative where ColumnB is considered "a correct value" and ColumnA is considered to be some kind of an error.

I tried

instance Applicative Choice where
    pure             =  ColumnB  
    ColumnB f  <*>  r  =  fmap f r
    ColumnA f  <*>  _  =  ColumnA  f   --- this does not work 

How can I make it work ?



Solution 1:[1]

Let's rename your data constructors to express your intent properly, as

data Choice a = Bad a | Good a

Your Functor instance keeps the taint on the values,

instance Functor Choice where 
  fmap f (Bad  x)  =  Bad  (f x) 
  fmap f (Good x)  =  Good (f x) 

so let's just do the same for the Applicative, without being skimpy with our clauses:

instance Applicative Choice where
    pure              x  =  Good    x     -- fmap f == (pure f <*>) is the Law
    Good f  <*>  Good x  =  Good (f x)
    Good f  <*>  Bad  x  =  Bad  (f x)
    Bad  f  <*>  Good x  =  Bad  (f x)
    Bad  f  <*>  Bad  x  =  Bad  (f x)

As was pointed in the comments, this interprets Choice a as isomorphic to Writer All a, meaning, Choice a values are really just like (Bool, a) with (False, x) corresponding to Bad x and (True, x) corresponding to Good x. Naturally we only consider values to be Good if everything in their provenance was Good as well.

Solution 2:[2]

If ColumnA is considered some kind of error, you can not let it wrap an a value. Indeed. The idea of (<*>) is that it takes a Choice (x -> y) and Choice x, and returns a Choice y. But if you have a ColumnA that wraps a function of type x -> y, and you have at the right hand a Choice x, then it thus should return a Choice y, not a Choice x.

What you could do is define a type with two type parameters, for example:

data Choice a b = ColumnA a | ColumnB b

then you only perform a mapping over the ColumnB b data constructor:

instance Functor (Choice a) where
    fmap _ (ColumnA e) = ColumnA e
    fmap f (ColumnB x) = ColumnB (f x)

and then we can define an Applicative instance as:

instance Applicative (Choice a) where
    pure = ColumnB
    ColumnB f <*> ColumnB x = ColumnB (f x)
    ColumnA e <*> _ = ColumnA e
    _ <*> ColumnA e = ColumnA e

Such instance for a Functor and Applicative however already exist: this is how it is defined on the Either data type.

Solution 3:[3]

I made a package for deriving Applicative for sum types:idiomatic.

Choice can be biased towards the left or the right, if it is biased towards the left then ChoiceA is the pure constructor and combining A an B defects to ChoiceB:

{-# Language DerivingVia #-}
{-# Language DerivingStrategies #-}
{-# Language DeriveGeneric #-}
{-# Language DataKinds  #-}

import Generic.Applicative
import GHC.Generics

data Choice a = ColumnA a | ColumnB a
  deriving 
  stock (Show, Generic1)

  -- pure :: a -> Choice a
  -- pure = ColumnA
  -- 
  -- liftA2 :: (a -> b -> c) -> (Choice a -> Choice b -> Choice c)
  -- liftA2 (·) (ColumnA a) (ColumnA a') = ColumnA (a · a')
  -- liftA2 (·) (ColumnA a) (ColumnB b)  = ColumnB (a · b)
  -- liftA2 (·) (ColumnB b) (ColumnA a)  = ColumnB (b · a)
  -- liftA2 (·) (ColumnB b) (ColumnB b') = ColumnB (b · b')
  deriving (Functor, Applicative)
  via Idiomatically Choice '[LeftBias Id]

and right-bias means ChoiceB is the pure constructor and combining A an B defects to ChoiceA:

  -- pure :: a -> Choice a
  -- pure = ColumnB
  -- 
  -- liftA2 :: (a -> b -> c) -> (Choice a -> Choice b -> Choice c)
  -- liftA2 (·) (ColumnA a) (ColumnA a') = ColumnA (a · a')
  -- liftA2 (·) (ColumnA a) (ColumnB b)  = ColumnA (a · b)
  -- liftA2 (·) (ColumnB b) (ColumnA a)  = ColumnA (b · a)
  -- liftA2 (·) (ColumnB b) (ColumnB b') = ColumnB (b · b')
  deriving (Functor, Applicative)
  via Idiomatically Choice '[RightBias Id]

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Will Ness
Solution 2 luqui
Solution 3 Iceland_jack