The user 'singpolyma' asked on reddit if there was some general structure underlying:
data FailList a e = Done | Next a (FailList a e) | Fail e
A free monad was suggested, but I wondered if this could be modeled more generally via applicative functors. In Abstracting with Applicatives, Bazerman shows us that the sum of two applicative functors is also an applicative functor, with bias to the left/right, provided we have a natural transformation in the direction of the bias. This sounds like it's what we need! Thus, I started my proposal, but then quickly ran into problems. Can anyone see solutions to these problems?:
Firstly, we start with the definition of the sum of two functors. I started here because we want to model sum types - either successes or successes and a failure.
data Sum f g a = InL (f a) | InR (g a)
And the two functors we want to work with are:
data Success a = Success [a]
data Failure e a = Failure e [a]
Success
is straight forward - it's essentially Const [a]
. However, Failure e
I'm not so sure about. It's not an applicative functor, because pure
doesn't have any definition. It is, however, an instance of Apply:
instance Functor Success where
fmap f (Success a) = Success a
instance Functor (Failure e) where
fmap f (Failure e a) = Failure e a
instance Apply (Failure e) where
(Failure e a) <.> (Failure _ b) = Failure e a
instance Apply Success where
(Success a) <.> (Success b) = Success (a <> b)
instance Applicative Success where
pure = const (Success [])
a <*> b = a <.> b
Next, we can define the sum of these functors, with a natural transformation from right to left (so a left bias):
instance (Apply f, Apply g, Applicative g, Natural g f) => Applicative (Sum f g) where
pure x = InR $ pure x
(InL f) <*> (InL x) = InL (f <*> x)
(InR g) <*> (InR y) = InR (g <*> y)
(InL f) <*> (InR x) = InL (f <.> eta x)
(InR g) <*> (InL x) = InL (eta g <.> x)
And the only thing we now have to do is define our natural transformation, and this is where it all comes crumbling down.
instance Natural Success (Failure e) where
eta (Success a) = Failure ???? a
The inability to create a Failure
seems to be the problem. Furthermore, even being hacky and using ⊥ isn't an option, because this will be evaluated, in the case where you have InR (Success ...) <*> InL (Failure ...)
.
I feel like I'm missing something, but I have no idea what it is.
Can this be done?
I'm pretty sure the "correct" answer is to make e
a monoid, much as you disliked the idea on the reddit discussion.
Consider Failure "oops" [(*1),(*2),(*3)] <*> Failure "doh" [1,2,3]
Should the result have "oops" or "doh" as the failure? By making the e
a monoid we capture the fact that there's no canonical choice, and let the consumer pick their poison (be it First
, Last
, []
, etc.)
Note that this solution, much like the (Maybe e, [a])
representation, doesn't deal correctly with streaming/potentially-infinite data, since it is strict in whether we have a failure ending the list.
A different encoding would use fixpoints of applicatives, as per the followup post (http://comonad.com/reader/2013/algebras-of-applicatives/).
Then you take the list representation presented there (FixF (ProductF Embed (Sum (Const ()))) a
) and alter it by sticking your error monoid in the unit position, to get the following:
Monid mon => FixF (ProductF Embed (Sum (Const mon))) a
And note that you can use a Maybe
instead of a monoid to get exactly a FailList
, but just as with FailList
you then don't get an applicative instance for free unless you write one specifying the right way to combine errors.
Also note that with the fixpoint approach if we have the equivalent of Success [(*1),(*2),(*3)] <*> Failure "doh" [1,2,3,4,5]
then we get back a Success
with three elements (i.e. we are genuinely nonstrict in failure) while in the approach you suggest, we get back a Failure
with three elements and the error from the five element failing list. Such are the tradeoffs of streaming vs. strict.
Finally, and most straightforwardly, we can just take type FailList e = Product (Const (First e)) ZipList
to use standard applicative machinery and get something very close to the original data type.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With