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?
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.
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.
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.
精彩评论