PureScript by Example - StateT and <*>?

#1

In the Monad Transformers section, in the Monadic Adventures chapter in PureScript by Example there’s this example

split :: StateT String (Either String) String
split = do
  s <- get
  case s of
    "" -> lift $ Left "Empty string"
    _ -> do
      put (drop 1 s)
      pure (take 1 s)

which I understood. Doing something like runStateT split "test" makes sense also.

However, runStateT ((<>) <$> split <*> split) "test" froze my brain.

The book says this means that we apply split twice to read the first two characters from a string.

How is this the application of split twice? I couldn’t wrap my head around it. Is there an intuitive explanation for this?

#2

<$> and <*> generalize n-ary function application in any Applicative context. It might make sense to see it in terms of a few other things that are equivalent. I’m going to use append instead, which is the named version of the <> operator.

We can expand out split to something equivalent.

split' = do
  result <- split
  pure result

This is just calling split, getting the result, and boxing it up again. We can also call it again, and do something with both results.

split'' = do
  result1 <- split
  result2 <- split
  pure (append result1 result2)

This is straightforward and a useful pattern, but it’s also a lot of noise if it’s something we need to do a lot. So maybe we can generalize this a bit by moving our concrete assumptions (append and split) into arguments. This will give us something we can reuse. I’m just using flub because I can’t think of a good name.

flub func action = do
  result1 <- action
  result2 <- action
  pure (func result1 result2)

split'' = flub append split

But do we always want to use the same action for both arguments? It’s likely that we might want to supply two different actions. So lets remove that assumption and add another argument.

flub2 func action1 action2 = do
  result1 <- action1
  result2 <- action2
  pure (func result1 result2)

split'' = flub2 append split split

We have to repeat split twice here, but you can probably see that this is more useful. What if I have a function that takes more than 2 arguments? Well, one solution is to copy and paste, and extend it:

flub3 func action1 action2 action3 = do
  result1 <- action1
  result2 <- action2
  result3 <- action3
  pure (func result1 result2 result3)

And so on, and so on… However, it’s not a lot of fun to copy and paste code. We can reduce this by utilizing currying, and defining flub3 in terms of flub2.

flub3 func1 action1 action2 action3 = do
  func2 <- flub2 func1 action1 action2
  result3 <- action3
  pure (func2 result3)

What is happening here? If I call flub2 with a function that takes 3 arguments, it will apply 2 of them and give me back a function that takes another argument. This helps with the boilerplate, and now I can keep adding flubs for more arguments.

flub4 func1 action1 action2 action3 action4 = do
  func2 <- flub3 func1 action1 action2 action3
  result4 <- action4
  pure (func2 result4)

This looks pretty much exactly like flub3, so that likely means we have something we can generalize. Lets ignore the specific arguments for the moment and look at what both of them are doing:

  • Running some action to get a function out of it
  • Running another action to get some value out of it
  • Applying the function to the value

So lets take out the specific arguments and generalize according to this commonality:

flubFn action1 action2 = do
  func <- action1
  result <- action2
  pure (func result)

flub2 f a b = flubFn (map f a) b
flub3 f a b c = flubFn (flub2 f a b) c
flub4 f a b c d = flubFn (flub3 f a b c) d

This particular implementation of flubFn is called ap in the Prelude, which is apply from the Applicative class implemented in terms of bind. There is actually a law that says if something is both Monad and Applicative, then apply and ap have to agree. apply has an operator alias of <*>, and map has the alias <$>.

split'' = apply (map append split) split
split'' = map append split `apply` split
split'' = map append split <*> split
split'' = append <$> split <*> split
split'' = (<>) <$> split <*> split
4 Likes
#3

Wow, thanks for the exhaustive explanation. I think I get this … to some extent. I need some time to internalize it :slight_smile:
My main area of confusion was, in the end, conceptualizing that combining the two split function invocation under the State monad means that both calls will work with the same state (if this made sense :slight_smile:).

#4

You’ll see this sort of behavior for anything that is a Monad (since Applicative and Monad must agree in this case). That is, Applicative operations will have a sequential left-to-right behavior like you’d get by using bind or do syntax. This isn’t the case in general though, and you can certainly have Applicatives that aren’t shared, and instead represent parallel branching. One of the distinguishing characteristics of Applicative vs Monad is that you cannot introduce dependencies between effects, so they can effectively be reasoned about in parallel.