review2
Last time, we took a stroll down memory lane to review some ancient code I wrote, and see what/how I’d do things differently now. That was fun, and I have a new banger for us today. Today’s example comes from the same place — a little vim-like text editor I was working on. Vim has this “repeat” operator which just does the same thing you did last time. It’s nice. Delete a word. Running repeat deletes another word. Kinda dinky in this example, but it can also rerun “increment every number in the next four paragraphs.” Anyway, so that’s the feature I was trying to duplicate. But the action I was running might have done some IO, and I didn’t want to repeat the IO (because maybe the IO was to gather some input from the user, and repeating shouldn’t ask again.) I was still high on my own supply of writing monads , so I approached this problem with a custom monad. The gist of the API was: I’m actually pretty impressed with past-me on this one. That’s a clean API, and the implementation was pretty cute. The trick here is to keep and a instances around. This code uses the to pop off IO actions it’s already run, and the to keep track of new IO actions it needs to cache. The “magic” happens in , which runs it through once with an empty state, and then returns a new action with the MonadWriter results moved into the state for the next time around: Simple. Clean. Elegant. Too bad it doesn’t actually work. The following program crashes with an error coming from the unsafe use of in : The attack here comes from the fact that we can force the program down a different code path its second time around. And that second time, it encounters a call to which has a different type than the one it cached. Thus, trying to reuse the cache leads to what is effectively a type error at runtime. What actually went wrong here? The problem is that monads are too powerful . Since the only way to compose monads is via bind ( ), we are forced to extend a monadic value of type by a function of type . Which is to say that the “next thing to do” in a monadic computation is always going to be a function. And functions are completely opaque. So, once we’re given a monad, there’s simply no way to know what it’s going to do without actually running the continuation. The problem arises from the fact that that continuation can invisibly branch. My monad was attempting to statically analyze the monadic computation, and cache the IO results in a queue. But as shows, it’s trivial to break this sort of static analysis. Because the branch is hidden inside of a function, and there’s no way to determine which branches a function didn’t take. But I didn’t know that eleven years ago, so I give myself a pass on this one. Nevertheless, let’s tackle this problem with the wisdom of ages. So if the problem with my previous implementation of is that it’s a monad, what options do we have? As it happens, there are two very nice options available to us here: arrows and selective applicative functors. We’ll discuss arrows for now, and come back to selectives. Long time readers might remember a series on arrows from a few years back. But if you don’t, that’s OK; you’re still welcome here. A quick recap: Arrows are a generalization of functions. Like functions, they take two type parameters (an input and an output). Like functions, they come with an identity arrow. They compose “end to end” just like functions do. These three properties are expressed as a superclass, which we usually call and write infix: In addition to being categories, arrows also come with a means of transforming regular functions into arrows, and of mapping arrows over pairs: What’s neat about arrows is that their values are function-like things, rather than being actually functions. That means when we compose arrows, we compose values whose internals we get to choose. Which in turn, means that static analysis is possible again! Attentive readers will notice that there isn’t actually any way for arrows-as-such to be able to branch. To gain that capability, they require an additional , which equips them with the ability to lift arrows over : To wet our whistle, let’s write a little helper that shows that the composition of an applicative functor with an arrow is itself an arrow: The arrow instances for are the usual trick of using applicative functions to lift operations into the right place. For example, identity is just liftA2` of composition: We can do the same trick for and : as well as for : What’s nice about this construct we’ve built is that it gives us two degrees of freedom. We’re free to choose an arrow whose structure we’re going to reuse, and an underlying applicative functor (which can give us “static” effects as we’re building the underlying arrow.) The idea here is that we can put the cache- building machinery inside of the applicative functor, but the cache- reading machinery inside of the underlying arrow. It’s worth knowing about the Kleisli arrow: which says that any monadic bind function is itself an arrow. Implementing the instances yourself is a fun exercise if you’re new to this stuff. We can reuse as our underlying arrow, which when you expand everything out, we get: What I think is particularly cool about this is that is just the composition of the newtype unwrappers, and yet its type is extremely reminiscent of my ten-year-ago implementation: This is an instance of a more general principle, which is that the final “observation” you want to make of your type is always a good representation of that type. It’s not the only representation, but it’s always a reasonable choice. Appropriately enough, this is known as a final encoding. Anyway. How can we implement ? By way of : works by constructing a at the level. Each call to gets a unique cache. Then we drop into the implementation of the arrow (which recall happens at runtime .) Inside the , we check to see if the map has a value at the requested , and if so, return the cached . If not, we run our function and cache it. Since there’s no tomfoolery here that is existentializing away our types, we don’t need to worry about the attack; Haskell’s type system guarantees we can’t construct such a thing. Because isn’t a , it can’t be a either. But it’s nice to be able to provide the equivalent of : So that completes our arrow-based implementation of . But there’s still something left undone. I said earlier that selective applicative functors would be an alternative approach to implementing . Selectives rightfully sit between and in the functor hierarchy, but were discovered too late , and everyone was still kind of annoyed about having had just stuck into the hierarchy. What, precisely is a selective functor? It’s a different solution to the problem of “monads are too powerful” which gives us a branching primitive to play with. Behold: says that you might have an , in which case you must run the provided . But maybe you have a , in which case you may run the effects of the . But nobody’s forcing you to. Since there are still no raw function continuations to be seen here, we can use to provide branching to our programs. But all of the branches can be identified statically, since we’re only operating on values, again, whose internals we can control. In fact, by aggressively nesting calls, you can get back a version of monadic bind — so long as you have a finite number of select calls you’d need to make: (the implementation of which is fun if you’re looking for a challenge) Anyway, all of this is to say that we could have equally structured our as a selective functor rather than as an arrow! But is at least as powerful as , so given all of our hard work already, we can pull out a instance for free. What do I mean when I say “at least as powerful?” It means we can derive it for free, given an instance. Behold, the newtype, which exists only for us to have something to attach some instances to. Here we use newtype deriving to say that if is a category/arrow/arrowchoice, then is too. But now we can show that having is enough to get and instance: Then, given an , we can get an implementation of : Here we don’t have the option of choosing to invoke in the case. But that’s OK, because in , doing so would correspond to creating caches for branches that don’t need it. Given all of this, we can now derive up to for : which means that users can now choose between the arrow hierarchy and the functor hierarchy for how they’d like to think about building actions. And to nudge them even a little further, we can introduce something that looks like a monad transformer, removing the input parameter: Clean. Tidy. Actually works this time around.