Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I model a list of successes with short circuiting failure via the composition of applicative functors?

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?

like image 873
ocharles Avatar asked Mar 17 '13 12:03

ocharles


1 Answers

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.

like image 144
sclv Avatar answered Oct 31 '22 13:10

sclv