Skip to content

Commit 291e498

Browse files
committed
better explanation of lift and liftIO
change phrasing of lift explanation add better motivation for liftIO
1 parent b4fc204 commit 291e498

File tree

1 file changed

+65
-6
lines changed

1 file changed

+65
-6
lines changed

content/monad-transformers.md

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ An `Either e a` wrapped in any other monad, i.e. `m (Either e a)`
3333

3434
[transformers](https://www.stackage.org/package/transformers) is a widely used package which provides transformer versions of various monads. It also provides two useful classes, `MonadTrans` and `MonadIO`.
3535

36-
`MonadTrans` makes it easy to embed one monad into another. All of the transformers defined in the `transformers` package are instances `MonadTrans`.
36+
Instances of `MonadTrans` are transformers which can be applied to other monads to create new monads. All of the transformers defined in the `transformers` package are instances of `MonadTrans`.
3737

3838
### MonadTrans
3939

@@ -89,17 +89,19 @@ Note that in this particular example, the use of `lift` in `lift getLine` is equ
8989

9090
Here's a (somewhat contrived) example that demonstrates the difference between `lift` and `liftIO` and the usefulness of the latter.
9191

92+
Suppose we added another layer to our transformer stack so that, instead of `MaybeT IO String`, we had `MaybeT (ExceptT MyPasswordError IO) String`.
93+
94+
As in our first example, we'd like to lift the `getLine` action into our transformer. Let's try.
95+
9296
```haskell
9397
getPassword' :: MaybeT (ExceptT MyPasswordError IO) String
9498
getPassword' = do
95-
password <- liftIO getLine
99+
password <- lift getLine
96100
guard (isValid password)
97101
return password
98102
```
99103

100-
In this variation we have more than one layer on top of `IO`. We have a `MaybeT` on top of `ExceptT` on top of `IO`. This is where `liftIO` helps us. We can use use `liftIO` to lift the `getLine` action into our stack no matter how deep `IO` is in our stack.
101-
102-
If we tried to use `lift` instead of `liftIO`, we'd see the following error:
104+
We get an error. Oops!
103105

104106
```
105107
Couldn't match type ‘IO’ with ‘ExceptT MyPasswordError IO’
@@ -109,8 +111,65 @@ In the first argument of ‘lift’, namely ‘getLine’
109111
In a stmt of a 'do' block: password <- lift getLine
110112
```
111113

112-
The error means we have another layer of our stack that we need to traverse before we can lift the IO action into our stack. In other words, we would need to do `lift (lift getLine)`. This is precisely what `liftIO` gives us. Doing `lift . lift . lift ...` is unmaintainable because it relies on the stack being a specific depth. If we decided to add another monad to our stack, our nested lifting would break. With `liftIO` we can short circuit this and simply lift the IO action all the way to the bottom of our stack.
114+
If we look at the type of `lift` when specialized to various transformers, we can see the problem.
115+
116+
```
117+
> :t \x -> (lift x :: MaybeT IO String)
118+
\x -> (lift x :: MaybeT IO String) :: IO String -> MaybeT IO String
119+
```
120+
121+
In this example, we can use `lift` to go from `IO` into our transformer. But with a deeper stack, we run into problems:
122+
123+
```> type MyDeeperStack = ReaderT Int (WriterT String IO) Bool
124+
> :t \x -> (lift x :: MyDeeperStack)
125+
\x -> (lift x :: MyDeeperStack)
126+
:: WriterT String IO Bool -> MyDeeperStack
127+
```
128+
129+
In other words, the `m` from `lift :: m a -> t m a` in our `MyDeeperStack` is `WriterT String IO`. So we would to need `lift` *again* in order to go from `IO Bool -> MyDeeperStack`, i.e.
130+
131+
```> :t \x -> ((lift . lift) x :: MyDeeperStack)
132+
\x -> ((lift . lift) x :: MyDeeperStack)
133+
:: IO Bool -> MyDeeperStack
134+
```
135+
136+
This is where `liftIO` helps us. It essentially lets us do a variable number of lifts. This lets us write less brittle code because if we decided to add yet another layer to our transformer stack, we wouldn't have to hardcode another call to `lift`.
137+
138+
As an example, what happens if we add a `MaybeT` to our stack?
113139

140+
```haskell
141+
type MyDeeperStack = ReaderT Int (WriterT String (MaybeT IO)) Bool
142+
```
143+
144+
`lift . lift` will no longer allow us to lift an `IO` action into our stack because we now have a third layer.
145+
146+
```> :t \x -> ((lift . lift) x :: MyDeeperStack)
147+
\x -> ((lift . lift) x :: MyDeeperStack)
148+
:: MaybeT IO Bool -> MyDeeperStack
149+
```
150+
151+
With `liftIO`, as is well:
152+
153+
```> :t \x -> (liftIO x :: MyDeeperStack)
154+
\x -> (liftIO x :: MyDeeperStack) :: IO Bool -> MyDeeperStack
155+
```
156+
157+
Want to add another layer? No problem:
158+
159+
```haskell
160+
type MyDeeperStack = ReaderT Int (WriterT String (MaybeT (ExceptT String IO))) Bool
161+
```
162+
163+
```> :t \x -> (liftIO x :: MyDeeperStack)
164+
\x -> (liftIO x :: MyDeeperStack) :: IO Bool -> MyDeeperStack
165+
```
166+
167+
Without `liftIO` we'd need to keep adjusting the number of lifts:
168+
169+
```> :t \x -> ((lift . lift . lift . lift) x :: MyDeeperStack)
170+
\x -> ((lift . lift . lift . lift) x :: MyDeeperStack)
171+
:: IO Bool -> MyDeeperStack
172+
```
114173

115174
* More transformer usage examples
116175
* Pitfalls of Writer laziness

0 commit comments

Comments
 (0)