Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is/Should wrapping functions into a monad transformer be considered bad practice?

Let's say we want to use ReaderT [(a,b)] over the Maybe monad, and then we want to do a lookup in the list.

Now an easy, and not too uncommon way to this is:

first possibility

find a = ReaderT (lookup a)

However it does seem like this asserts some non-trivial thing about how the ReaderT transformer works. Looking at the source code for Control.Monad.Reader it's clear that this works just fine. But I haven't read any documentation supporting this. However we could also write find like this:

second possibility

find a = do  y <- ask 
             lift (lookup a y)

Similar ideas hold for wrapping MaybeT, StateT, State and Reader. Usually I write something like the first example, but most of the time it is really obvious how to write it like the second example, and you might even say it's more readable. So my question is: should code like the first example be considered bad?

like image 841
HaskellElephant Avatar asked Nov 30 '10 18:11

HaskellElephant


3 Answers

I think the biggest problem with the first way is:

If the mtl authors (or whatever transformer library you use), decide to stop exporting the data constructor for ReaderT then it will stop working. This happened with the State monad in the version bump from mtl 1 to mtl 2 and it's quite annoying. Whereas, ask is part of the official api of Reader and you should plan on it sticking around.

On the other hand, I wouldn't consider the first way wrong.

like image 149
Jason Dagit Avatar answered Oct 16 '22 18:10

Jason Dagit


The current version of the mtl library - which is based on the transformers library - exports the function reader :: (r -> a) -> Reader r a for exactly this purpose when using the simple Reader monad. So we see that the design of the library does take into account this usage. Since no such function is provided for ReaderT, we can say with some confidence that the officially supported way to do this with ReaderT is to use the constructor directly.

I'll agree with you if you say that an analogous readerT :: Monad m => (r -> a) -> ReaderT r m a ought to be added to the library. That would be good both for consistency, and for allowing the possibility of changing the internal representation someday without breaking anyone's code.

But for now, your "first possibility" is the way to go.

like image 44
Yitz Avatar answered Oct 16 '22 18:10

Yitz


At least there is a speed difference.

I wrote a program, which uses a random gen as a state and must generate about 5000000 random values while running. Now consider these two functions, which roll a dice:

random16  = State $ randomR (1,6) -- Using the internal representation
random16' = do
            s <- get
            (r,s') <- randomR (1,6) s
            put s'
            return r

Whith the first one, the program runs in about 6 seconds, while the second one is much slower, taking about 8 seconds. I can image, that it is similar for reader, so maybe use this one instead of the more clearer when runtime is important. I used the strict version for this.

like image 41
fuz Avatar answered Oct 16 '22 19:10

fuz