Trying to compose non-composable: lift everything!

3 minute read

Published:

It’s recommended to read the first part: Trying to compose non-composable: joint schemes, but if you’re not interested in details of implementation - it’s okay, in this article I’ll quickly go through the capabilities of my joint library.

Introduction

According to Stephen Diehl, algebraic effects are one of the most important problems that will be solved in the future for Haskell and I would like to make my modest contribution.

Lift one effects to the others

What does lifting mean, actually? Well, it’s very similar to pure/return, but it makes not value effectful, but other effect:

pure :: a -> t a
lift :: u a -> t u a

So we can use any effects in some transfomer, but later we have to interpret them all.

Thus, we can easily compose the lifted effects:

let f = lift get :: Configured _ t => t _
let g = lift Nothing :: Optional t => t _
let h = lift (failure _) :: Failable _ t => t _

let x = f *> g *> h :: (Applicative t, Configured _ t, Optional t, Failable _ t) => t _

And represent them in any order we like:

let x = f *> g *> h :: (Applicative t, Configured _ t, Optional t, Failable _ t) => t _

let y = pure _ :: Reader _ :> State _ :> Either _ :> Maybe := _
let z = pure _ :: State _ :> Either _ :> Maybe _ :> Reader := _

let xy = x *> y :: Reader _ :> State _ :> Either _ :> Maybe := _
let xz = x *> z :: State _ :> Either _ :> Maybe _ :> Reader := _

Effects adaptation

Adaptation means that some effects can be replaced by more powerful effects. For example, Reader and Writer effects can be used in State because State can read and write, so it can modify stored value:

lift put :: Accumulated _ t => t _
lift get :: Configured _ t => t _
(lift . adapt $ put) :: Stateful _ t => t _
(lift . adapt $ get) :: Stateful _ t => t _

How is that possible? In the previous part, we divided State into two effects:

State s ~ (->) s :. (,) s

In case of Reader we just lift -> (arrow) functor, in case of Writer - , (tuple):

Reader s ~ (->) s
Writer s ~ (,) s

Also you can adapt Failable to Optional but we lost error information:

(lift $ Just _) :: Optional t => t _
(lift $ failure _) :: Failable _ t => t _
(lift . adapt $ failure _) :: Optional t => t _

Simple real world example

Let’s imagine that we need to make an HTTP request, it’s IO, that can throw HttpException :

IO - salesman

import qualified "wreq" Network.Wreq as HTTP

request :: (Monad t, Liftable IO t, Failable HttpException t) => t (Response ByteString)
request = lift (try @HttpException $ HTTP.get link) >>= lift

Wow, what is there? First, we lift some IO-action, then after >>= we lift Either and we get an expression, that can be used in many effectful expressions than contain such two effects! We can delay using concrete transformers until we really need to evaluate them.

Conclusion

No free/freer monad, no GADTs and other fancy stuff - all you need is a functor composition. If you want to be able to use your own effect you need to pick a joint schema and write several instances (Functor/Applicative/Monad).

The source code for joint is there. Thanks for reading, if you wanna try it or you have some questions - don’t hesitate to PM me.

Leave a Comment