There is a nice Free Alternative in the great free package, which lifts a Functor to a left-distributive Alternative.
That is, the claim is that:
runAlt :: Alternative g => (forall x. f x -> g x) -> Alt f a -> g a
is an Alternative homomorphism, with liftAlt
. And, indeed, it is one, but only for left-distributive Alternative instances.
Of course, in reality, very few Alternative instances are actually left-distributive. Most of the alternative instances that actually matter (parsers, MaybeT f for most Monad f, etc.) are not left-distributive. This fact can be shown by an example where runAlt
and liftAlt
do not form an Alternative homomorphism:
(writeIORef x False <|> writeIORef True) *> (guard =<< readIORef x)
-- is an IO action that throws an exception
runAlt id $ (liftAlt (writeIORef x False) <|> liftAlt (writeIORef True))
*> liftAlt (guard =<< readIORef x)
-- is an IO action that throws no exception and returns successfully ()
So runAlt
is only an Alternative homomorphism for some Alternatives, but not all. This is because the structure of Alt
normalizes all actions to distribute over the left.
Alt
is great because, structurally, Alt f
is a lawful Applicative
and Alternative
. There isn't any possible way to construct a value of type Alt f a
using the Applicative and Alternative functions that don't follow the laws...the structure of the type itself is what makes it a free Alternative.
Just like, for lists, you can't construct a list using <>
and mempty
that doesn't respect x <> mempty = x
, mempty <> x = x
, and associativity.
I've written a Free alternative that doesn't enforce the Applicative and Alternative laws, structurally, but does yield a valid Alternative and Applicative homomorphism with runAlt/liftAlt:
data Alt :: (* -> *) -> * -> * where
Pure :: a -> Alt f a
Lift :: f a -> Alt f a
Empty :: Alt f a
Ap :: Alt f (a -> b) -> Alt f a -> Alt f b
Plus :: Alt f as -> Alt f as -> Alt f as
instance Functor f => Functor (Alt f) where
fmap f = \case
Pure x -> Pure (f x)
Lift x -> Lift (f <$> x)
Empty -> Empty
Ap fs xs -> Ap ((f .) <$> fs) xs
Plus xs ys -> Plus (f <$> xs) (f <$> ys)
instance Functor f => Applicative (Alt f) where
pure = Pure
(<*>) = Ap
instance Functor f => Alternative (Alt f) where
empty = Empty
(<|>) = Plus
structurally, Alt f
is not an actual Applicative
, because:
pure f <*> pure x = Ap (Pure f) (Pure x)
pure (f x) = Pure (f x)
So pure f <*> pure x
is not the same as pure (f x)
, structurally. Not a valid Applicative, right off the bat.
But, with the given runAlt
and liftAlt
:
liftAlt :: f a -> Alt f a
liftAlt = Lift
runAlt :: Alternative g => (forall x. f x -> g x) -> Alt f a -> g a
runAlt f = \case
Pure x -> pure x
Lift x -> f x
Empty -> empty
Ap fs xs -> runAlt f fs <*> runAlt f xs
Plus xs ys -> runAlt f xs <|> runAlt f ys
And runAlt
here does indeed act as a valid Applicative homomorphism with a given natural transformation...
One could say that my new Alt f
is a valid Alternative and Applicative when quotiented by the equivalence relationship defined by runAlt
, i suppose.
Anyway, this is only slightly unsatisfying. Is there any way to write a free Alternative that is structurally a valid Alternative and Applicative, without enforcing left distributivity?
(In particular, I'm actually interested in one that follows the left catch law, and enforces it structurally. This would be a separate and also interesting thing, but not completely necessary.)
And, if there isn't any way, why not?
Control.Alternative.Free
'sAlt f
produces a left-distributiveAlternative
for free, even iff
isn'tAlternative
orf
is a non-left-distributiveAlternative
. We can say that, in addition to the well-agreed upon alternative lawsAlt f
also gives left-distribution for free.Because
Alt f
is always left distributive (andrunAlt . liftAlt = id
)liftAlt
can never be a homomorphism for non-left-distributiveAlternative
s. If anAlternative f
is not left-distributive, then there exista
,b
, andc
such thatIf
liftAlt : f -> Alt f
were a homomorphism thenTo demonstrate this we need an
Alternative
that isn't left-distributive. Here's one,FlipAp []
.Along with some laws for left distribution and right distribution, and some examples
We can demonstrate a few properties of lists,
FlipAp
lists, andrunAlt
.Lists are left-distributive, but
FlipAp
lists aren'tLists aren't right-distributive, but
FlipAp
lists areAlt
is always left-distributiveAlt
is never right-distributiveWe can defile a right-distributive free alternative in terms of
FlipAp
andAlt
.FlipAp
Alt
is never left-distributive.FlipAp
Alt
is always right-distributiveUp until now I haven't told you anything that you didn't already imply by saying that
liftAlt : f -> Alt f
is anAlternative
homomorphism, but only for left-distributive Alternative instances. But I have shown you a free-alternative that isn't left-distributive (it's trivially right-distributive instead).A structurally valid free
Alternative
This section answers the bulk of your question, is there a structurally valid free
Alternative
that isn't left-distributive? Yes.This isn't an efficient implementation; it's purpose is to demonstrate that it exists and that some version of it can be arrived at in a straight-forward manner.
To make a structurally valid free
Alternative
I am doing two things. The first is to create a data structure that can't represent any of theAlternative
laws; if it can't represent the law then a structure can't be constructed independently of the type class to violate it. This is the same trick used to make lists structurally obey theAlternative
associativity law; there's no list that can represent the left-associated(x <|> y) <|> z
. The second part is to make sure the operations obey the laws. A list can't represent the left association law, but an implementation of<|>
could still violate it, likex <|> y = x ++ reverse y
.The following structure can't be constructed to represent any of the
Alternative
laws.It's a
Functor
And it's
Applicative
. Because the structure can't represent the laws, when we encounter a term containing one of the unpreventable expressions we're forced to convert it into something else. The laws tell us what to do.All of those
Ap
s could be covered by a pair of view patterns, but it doesn't make it any simpler.It's also an
Alternative
. For this we'll use a view pattern to divide the cases into the empty and non-empty cases, and an extra type to store the proof that they're non-emptyliftAlt
andrunAlt
This new
Alt f
doesn't provide either left-distribution or right-distribution for free, and thereforerunAlt id :: Alt f a -> g a
preserves how distributiveg
is.Lists are still left-distributive, but
FlipAp
lists aren't.List's aren't right-distributive, but
FlipAp
lists still areSource code for this section
Structurally valid left-catch free
Alternative
To control which laws we want we can add them to the structurally free alternative we made earlier.
To add left-catch we'll modify the structure so it can't represent it. Left catch is
(pure a) <|> x = pure a
To keep
Alt'
from representing it we'll excludepure
from what's allowed to the left ofPlus
.This results in a compiler error in the implementation of
Alternative Alt
Which we can fix by appealing to our new law,
(pure a) <|> x = pure a