Infodumping About Selective Applicative Functors

By @MonoidMusician

2024/04/10

Originally posted at https://tech.lgbt/@monoidmusician/112250458187094582

tonightʼs infodump on selective applicative functors:

we know about monads, which have dynamic control flow: based on the result of a previous computation, you can invent a whole new computation to run in the chain

(>>=) :: m x -> (x -> m y) -> m y

this is the most flexible you can be, and it necessitates a linear order of evaluation

applicatives have static control flow: the set of computations to run is fixed up front, and then you get to combine their results at the end

applicatives can evaluate however they like: sequentially, in parallel … they can even evaluate right-to-left (like monoids, they can be flipped via ^op)

(<**>) :: m x -> m (x -> y) -> m y

alternatives model nondeterminism: you donʼt write into the program what branch to choose, but however you interpret the functor has to decide

this is useful for parsers: the parser will return results from branches that match, discarding branches that fail

(<|>) :: m x -> m x -> m x

selective applicative functors lie in between monads and applicatives in terms of their power, and show similarities with alternatives too

unfortunately, selective applicatives are tricky to formulate correctly, but the basic flavor is given by select and branch:

select :: m (Either u v) -> m (u -> v) -> m v
branch :: m (Either x y) -> m (x -> z) -> m (y -> z) -> m z

one way to think of it from a high level is that selective applicative functors let you have determined choice:

the computation itself gets to determine what branch to take, by whether it returns a Left or a Right

it is no longer nondeterministic like <|>

but it also isnʼt completely dynamic like monads: you have to declare all the branches up front, make some dynamic choices along the way, and use all the information to come up with a final result


for several months Iʼve been thinking about selective applicative functors in the context of parsers specifically

I realized that thereʼs a reason that <|> and branch feel similar in terms of both being branching structures of control flow, and the implications that has for scoping and syntax and such:

selective parsers are just parsers with dynamic failure: try :: m x -> (x -> Maybe y) -> m y, where you get to inspect a result but choose to make the parser fail anyways

parsers with (finite!) case matching built in, and just the intuitive notion of scope that that brings

I posted some notes about why this is on cohost:

https://cohost.org/monoidmusician/post/2588944-more-more-more-seman

and my notes on selective functors more generally:

https://cohost.org/monoidmusician/tagged/selective%20applicative%20functor

and Iʼve been developing a framework for parser combinators that will hopefully execute selective applicative structures with LR(1) or GLR tables

(uhhh I havenʼt figured out if it corresponds to a known class of grammars yet)


so why is it so tricky to formulate?

select :: m (Either u v) -> m (u -> v) -> m v
branch :: m (Either x y) -> m (x -> z) -> m (y -> z) -> m z

select is weird: Either is a tensor, but -> sure as heck isnʼt.

an alternative formulation goes like

m u -> m (Either v (u -> v)) -> m v

where you have a “maybe constant function” (Either v (u -> v)), but this still doesnʼt fit into category theory nicely at all

branch is even weirder: it looks like it has the right shape, but it doesnʼt compose!!

the 2-ary branch does not easily give rise to 3-ary branch, you basically have to reimplement select in terms of branch and then apply it multiple times, it is so annoying

my solution and notes on why this is is here:

https://github.com/MonoidMusician/blog/blob/main/PureScript/src/Parser/Selective.purs

basically you have to treat it like the control flow it is: break out the profunctors/arrows, specifically have a profunctor for “finite case branching on some type”, and then use that as your building block for a N-ary branch


so the formulation I like says, “follow a computation with a finite case tree on its result, whose result is the result of the whole computation”:

caseTreeOn :: forall i r. f i -> CaseTree i f r -> f r

(CaseTree is tricky to define, with existential quantification … I think thatʼs part of why nobody has stumbled across it to my knowledge)

these are strong functors, so you can pass down any information you learned in the first computation into the branches of the second … basically how you would expect variable scoping to work (in non-linear type theories) :)

(yes you can shut up about how monads need to be strong to be applicative functors: everything is strong here.)


this is a strengthening of the usual definition of selective functors (which just use select): it encodes exclusive determined choice

now, itʼs a bit tricky to answer what this gives you, what any of this gives you,

but exclusive determined choice is important for parsers, at least: for the purposes of static analysis, you would like to know that two branches are exclusive, to know that ambiguities between them will always be resolved

you simply do not get this knowledge with select: if you look at how to encode branch via select, youʼll realize that you have to inspect the control flow of (potentially) arbitrary functions to make that determination, which is absolutely forbidden

(basically it works by saying that each branch “could” be run, and shuffling around data to ensure that the right branch gets chosen or skipped each time … it is really annoying and inelegant)


now would be a great time to clarify this:

nothing in the formalism says that branches have to be skipped, and this is a feature not a bug

I will repeat

branches can be executed even if they were not chosen, and this is on purpose

in the case of select, it means that select can be implemented in terms of <*> which always runs both branches! (and just discard the result of the one that was not necessary)

I will get more into this when I ask “what does this formalism get us, anyway?”

but for now, observe that the beauty (yes, beauty) of selective applicative functors is that they are more plentiful:

whereas types mostly have one monad instance, and maybe a couple different applicative instances, they will have a proliferation of selective instances:

lists have their usual monad structure which gives an applicative structure (Cartesian product), but they also have a ziplist structure, and then their flipped/^op versions

all these give different selective structures, and more!


another example: the Const functor has no monad structure, but it has a standard applicative structure given by monoid concatenation (and its opposite order, which we do not care about)

but as a selective functor we can work some magic: Under with only capture the effect executed unconditionally (i.e. the first arguments of select, ignoring the second arguments) and Over will take it all, regardless of if it would be ignored during runtime (this is the selective structure given by the applicative)

this is in The Paper, read more there (section 2.2): https://dl.acm.org/doi/10.1145/3341694

but it starts to answer our question: why is this useful?

itʼs useful because you can approximate the effects you are about to run, in advance: they all exist statically, the control flow can be dynamic, but itʼs tailor made so you can ignore the control flow and know everything that might pop up


itʼs tempting to impose a law that branches of control flow that are never taken are always ignored

but there is a huge problem with that: to obey the law, an implementation either needs to (1) inspect arbitrary functions to determine if the next computation could possibly run, or (2) actually execute the one or many possibilities and keep track of the results … i.e. run/simulate the computation.

both of those are non-starters for static analysis, it simply ruins the abstraction we are going for

you might ask, “why not formulate static analysis as a separate abstraction, and leave selective functors for the well-behaved ones?”,

The Paper tells you why!!

through the example of Haxl (a library mostly meant for DB queries) shows that you want it to live in a single abstraction so that you can have static analysis interleaved with dynamic execution

how cool is that??

basically batches DB queries to get all required data and maybe conditionally required data, Iʼm fuzzy on the details still


so yeah, if you let it all live under one abstraction, you get nice things

like, you can take the product of two selective applicative functors,

so you can just tuple up your over-approximation, your under-approximation, and your actual execution context into one functor, simply because they all worked on their own …

… or you can find ways to interleave them like Haxl did

the main problem with Haxl, which is not its fault but rather Haskellʼs, is that Haskell typeclasses require that instances agree with each other:

if a type implements Monad, it needs to implement Applicative in a closely compatible way

and the same choice was made with Selective: must agree with Monad instance if it exists

I think this is mostly good design, itʼs important that things behave predictably

itʼs just so tempting to cut corners and implement more interesting instances for Applicative and Selective, mostly because no language nor library features make dealing with it easier


Haxl in particular got in hot water for having an Applicative instance that didnʼt agree with its Monad instance, since the Applicative wants to do static analysis and batch everything, while Monad simply does not allow batching

we should support this somehow! thereʼs a really interesting story to be told there! we do want to prefer static analysis when possible!

itʼs just hard to express currently (it requires making newtypes for each behavior you think of), and I really think we need to make this easier at the language+library level (itʼs something that I want to address in tmTTmt!)

there is Parallel in PureScript, which gives parTraverse and other goodies, but idk Iʼve never enjoyed making new types for it……


okay so as the final word in what selective applicative functors get us in terms of static analysis:

my case tree abstraction makes it more clear

the whole deal with regular applicative functors is that their static analysis is just a monoid, given by the Const instance

in fact, I would argue thatʼs all that static analysis is: an interpretation of the functor f a -> c that is lawful as a morphism of the appropriate structure of type f a -> Const c a.

with caseTreeOn, we donʼt just have a monoid anymore: we still combine effects sequentially with a monoid, but we combine the alternatives with a (potentially differently behaved) semigroup

(we can ignore empty case trees: 0-ary branch just evaluates its first argument)

thereʼs probably some laws you want to impose between the monoid and semigroup

in fact, having a lattice or semilattice is particularly nice

and semilattices crawl all over static analysis, you wouldnʼt believe it

(well, maybe you would ^^)


and as a final message that should have been a first message for intuition:

what type of do syntax would we give to selective applicatives?

for monads we have regular do syntax:

do
  x <- f
  y <- g x
  case x + y of
    42 -> return True
    r -> loop r

for applicatives we have much stricter ado notation, where each binding only is visible in the final in clause (return statement):

ado
  x1 <- c1
  x2 <- c2
  in x1 + x2

selective will allow case branches, where the bound variables are visible in case and in but still not the right sides of binds:

ado
  x1 <- c1
  x2 <- c2
  case x1 + x2 of
    42 -> pure True
    r -> ado
      -- x1, x2, and r are still not in scope
      x3 <- c3
      -- until `in`
      in x1 + x2 + x3 == 42

unfortunately the desugaring will be a lot more complicated: the compiler essentially needs to pick a strategy for pattern matching and expose it during desugaring

thereʼs also details in branching in the middle vs at the end, yeah …