One often wants to execute a sequence of commands and collect the sequence of their responses, and indeed there is such a function in the Haskell Prelude (here specialised to IO)

sequence :: [ IO a ] → IO [a ] sequence [ ] = return [ ] sequence (c : cs) = do x ← c xs ← sequence cs

Before we get started, if you’re following along in your Scala IDE or REPL you will need some imports listed below. You can also clone the Github repository.

import zio. _ import zio.console. _ import zio.clock. _ import zio.duration. _ import cats. Applicative import cats.implicits. _

… and the following libraries …

libraryDependencies ++= Seq ( "org.typelevel" %% "cats-core" % "2.1.1" , "dev.zio" %% "zio" % "1.0.0-RC18" )

I am using ZIO in place of Haskell’s IO Monad, and bringing in Cats to use its Applicative.

Converting the sequence function from Haskell to Scala…

def monadicSequence [ Z , E , A ](ios : List [ ZIO [ Z , E , A ]]) : ZIO [ Z , E , List [ A ]] = { ios match { case Nil => zioApplicative.pure( List .empty[ A ]) case c :: cs => for ( x <- c; xs <- monadicSequence(cs) ) yield (x +: xs) } }

If you’re not familiar with ZIO you can think of it as a replacement for the standard library Scala Future , but it has better performance and a lot more features. It is also not eagerly evaluated like Future. To explain, when you create a future it runs immediately and you cannot run it again. You can create a ZIO and run it when you decide to and as many times as you want.

To demonstrate this sequence running let’s write an implementation of a silly algorithm called Sleep Sort. Sleep Sort works by waiting an amount of time based on the value of the number. Emitting the numbers in this way sorts them (assuming your scheduler is accurate enough). Let’s be clear, this is a stupid way to sort numbers, but it’s handy as a way to illustrate our monadicSequence function.

def delayedPrintNumber (s : Int ) : ZIO [ Console with Clock , String , Int ] = { putStrLn(s "Preparing to say number in $s seconds" ) *> putStrLn(s " $s " ).delay(s.seconds) *> ZIO .succeed(s) } val ios1 = List ( 6 , 5 , 2 , 1 , 3 , 8 , 4 , 7 ).map(delayedPrintNumber) // ios1: List[ZIO[Console with Clock,String,Int]]

The function creates an IO effect, which when run will immediately print a message and then wait s seconds before printing the number. We map the function across a list of numbers to generate a list of IO effects, which we can then run.

You may be surprised that this does not work. Instead of running all the effects at once and printing them out in order it just executes the first IO (wait 6 seconds), then the second (wait 5 seconds).

Monadic version Preparing to say number in 6 seconds 6 Preparing to say number in 5 seconds 5 // ... and so on for a while

If you were not surprised maybe you’re ahead of me, and know that our monadicSequence function cannot possibly run all the effects at once by virtue of it being monadic in the first place.

That for comprehension is really hiding that we are calling flatMap on each successive IO, and flatMap sequences things together. You must wait for the result of the first effect before you can evaluate the second. So whilst the first implementation of sequence in the paper will absolutely work, it will not let us implement our sleep sort, nor let us parallelize the IO’s in general.

Back to the paper, at this point the authors observe…

In the (c : cs) case, we collect the values of some effectful computations, which we then use as the arguments to a pure function (:). We could avoid the need for names to wire these values through to their point of usage if we had a kind of ‘effectful application’.

By effectful application they are talking about the ap function, and they go on to say that it lives in the Haskell Monad library. Given that function they rewrite the sequence function as follows…

sequence :: [ IO a ] → IO [a ] sequence [ ] = return [ ] sequence (c : cs) = return ( : ) ‘ap‘ c ‘ap‘ sequence cs

Except for the noise of the returns and aps, this definition is in a fairly standard applicative style, even though effects are present.

Note that the ap they are using here is in the Monad library, and implemented using flatMap, so it will not yet allow our sleep sort to work. However, I’ve implemented an Applicative instance for ZIO which does not have that limitation…

implicit def zioApplicative [ Z , E ] = new Applicative [ ZIO [ Z , E ,?]] { def pure [ A ](x : A ) = ZIO .succeed(x) def ap [ A , B ](ff : ZIO [ Z , E , A => B ])(fa : ZIO [ Z , E , A ]) = { map2(ff, fa){ (f,a) => f(a) } } override def map2 [ A , B , C ](fa : ZIO [ Z , E , A ], fb : ZIO [ Z , E , B ])(f : ( A , B ) => C ) : ZIO [ Z , E , C ] = { fa.zipPar(fb).map{ case ( a , b ) => f(a,b)} } }

It’s not important to understand all the details here, all you need understand is we now have an ap that we can apply to ZIO effects that is truly parallel, so if you’re not interested then skip to the next paragraph.

The pure function is straightforward, it just wraps a pure value in a succeeded ZIO. The ap function is more interesting. Whilst it’s not obvious how you would implement ap in for ZIO, it is really easy to implement map2 . map2 comes in handy because it lets you take the results of two effects and pass them to a pure function. The function has the signature f: (A, B) => C . We use the ZIO function zipPar to execute the two effects in parallel, and if both fa and fb yield values then they are mapped with the pure function giving us a ZIO with the final result inside. Happily, you can implement ap in terms of map2, so that solves our problem.

Here’s the conversion of the applicative version of sequence to Scala…

def applicativeSequence [ Z , E , A ](ios : List [ ZIO [ Z , E , A ]]) : ZIO [ Z , E , List [ A ]] = { ios match { case Nil => ZIO .succeed( List .empty[ A ]) case c :: cs => val ff : ZIO [ Z , E , A => ( List [ A ] => List [ A ])] = zioApplicative.pure(((a : A ) => (listA : List [ A ]) => a +: listA)) val p1 = ff.ap(c) p1.ap(applicativeSequence(cs)) } }

It’s a little bit noisier than the Haskell code, but most of that is having to be more verbose about the types to keep the type checker happy. In fact the parts of each implementation match up together.

Now we can run that and you will see that the effects are now parellelised and our sleep sort works!

Applicative version Preparing to say number in 6 seconds Preparing to say number in 2 seconds Preparing to say number in 1 seconds Preparing to say number in 3 seconds Preparing to say number in 8 seconds Preparing to say number in 4 seconds Preparing to say number in 7 seconds Preparing to say number in 5 seconds 1 2 3 4 5 6 7 8