forked from orion/obsidian
209 lines
4.5 KiB
Markdown
209 lines
4.5 KiB
Markdown
Do notation is sugar for [[Bind|bind]] `>>=`, allowing imperative blocks that use bind as the driver of the "and-then" behavior for the specific type being returned.
|
|
|
|
```haskell
|
|
main :: Effect Unit
|
|
main = do
|
|
log "hello, world!"
|
|
log "foo bar baz"
|
|
|
|
-- logs:
|
|
-- hello, world!
|
|
-- foo bar baz
|
|
```
|
|
|
|
Do blocks are opened with the `do` keyword and support only a few operations:
|
|
|
|
- [[#bind|"await" with <-]]
|
|
- [[#let-bindings|define local variables]]
|
|
- [[#eval and ignore|evaluate an expression but discard its output]]
|
|
- [[#return|return an expression]]
|
|
|
|
statements in do blocks are evaluated line-by-line top-to-bottom, just like any other imperative language.
|
|
|
|
## bind
|
|
The left-arrow `<-` in `do` blocks is analogous to the `await` keyword in JS, C# & Java, and the question mark operator in Rust.
|
|
|
|
```haskell
|
|
intPositive :: Int -> Maybe Int
|
|
intPositive n
|
|
| n > 0 = Just n
|
|
| otherwise = Nothing
|
|
|
|
f :: String -> Maybe Int
|
|
f s = do
|
|
-- if `s` can't be parsed, returns Nothing early
|
|
num <- Int.fromString s
|
|
-- if `num` is <= 0, returns Nothing early
|
|
pos <- intPositive num
|
|
Just pos
|
|
```
|
|
|
|
This desugars to:
|
|
```haskell
|
|
f s =
|
|
Int.fromString s
|
|
>>= ( \num ->
|
|
intPositive num
|
|
>>= (\pos -> Just pos)
|
|
)
|
|
```
|
|
|
|
## let-bindings
|
|
Related to [[let .. in ..]], You can define new local variables in the middle of do blocks that can depend on previous bindings:
|
|
|
|
```haskell
|
|
do
|
|
a <- Just 1
|
|
let
|
|
b = a + 1
|
|
c = b + 1
|
|
Just c
|
|
-- evaluates to (Just 3)
|
|
```
|
|
|
|
> [!attention]- Unexpected or mismatched indentation
|
|
> A common gotcha with let-bindings in do blocks is only indenting once after trying to insert a line break after the equals:
|
|
> ```haskell
|
|
> do
|
|
> a <- Just 1
|
|
> let b =
|
|
> a + 1
|
|
> Just b
|
|
> ```
|
|
>
|
|
> ```text
|
|
> [ERROR 1/1 ErrorParsingModule] ...
|
|
>
|
|
> 4 a + 1
|
|
>
|
|
>
|
|
> Unable to parse module:
|
|
> Unexpected or mismatched indentation
|
|
> ```
|
|
>
|
|
> This happens because oneline let bindings
|
|
> ```haskell
|
|
> do
|
|
> let a = b
|
|
> ..
|
|
> ```
|
|
> are identical to
|
|
> ```haskell
|
|
> do
|
|
> let
|
|
> a = b
|
|
> ..
|
|
> ```
|
|
> So when you break after the equals and only indent once:
|
|
> ```haskell
|
|
> do
|
|
> let a =
|
|
> b
|
|
> ..
|
|
> ```
|
|
> what you're actually writing is
|
|
> ```haskell
|
|
> do
|
|
> let
|
|
> a =
|
|
> b
|
|
> ..
|
|
> ```
|
|
|
|
## eval and ignore
|
|
When an expression returns `m Unit`, you don't have to write an arrow:
|
|
```haskell
|
|
guard :: Boolean -> Maybe Unit
|
|
guard true = Just unit
|
|
guard false = Nothing
|
|
|
|
do
|
|
n <- Int.fromString "123"
|
|
-- when `n > 0`, this resolves to `Just unit` and
|
|
-- the execution continues.
|
|
-- If this is Nothing, the block short-circuits
|
|
-- and returns Nothing.
|
|
guard (n > 0)
|
|
Just n
|
|
```
|
|
|
|
If you want to discard a value other than unit, you need to use the discard syntax `_ <- x`
|
|
|
|
```haskell
|
|
main :: Effect Unit
|
|
main = do
|
|
fooTxt <- File.createWriteStream "foo.txt"
|
|
-- Stream.writeString returns a Boolean
|
|
-- indicating whether the stream needs us
|
|
-- to wait for the `drain` event before writing
|
|
-- more, but we don't care here.
|
|
_ <- Stream.writeString UTF8 "foo" fooTxt
|
|
pure unit
|
|
```
|
|
|
|
## return
|
|
the last statement in a do block is the expression the do block will return, and can't be a let-binding or bind statement:
|
|
|
|
```haskell
|
|
-- block immediately resolves with `Just ""`
|
|
do
|
|
Just ""
|
|
```
|
|
|
|
```haskell
|
|
-- block resolves with `Just foo`
|
|
do
|
|
foo <- getFoo
|
|
Just foo
|
|
```
|
|
|
|
```haskell
|
|
-- block resolves with `Just "foo"`
|
|
do
|
|
do
|
|
do
|
|
do
|
|
Just "foo"
|
|
```
|
|
|
|
## Examples
|
|
`do` in [[Maybe]] allows us to return early with empty values.
|
|
|
|
In this example, `getApiUrlPort` will return Nothing if:
|
|
- environment variable `API_URL` is unset
|
|
- `API_URL` is not a valid URL
|
|
- `API_URL` is a valid URL, but the port isn't set
|
|
|
|
```haskell
|
|
lookupEnv :: String -> Maybe String
|
|
urlFromString :: String -> Maybe URL
|
|
urlPort :: URL -> Maybe Int
|
|
|
|
getApiUrlPort = do
|
|
apiUrlString <- lookupEnv "API_URL"
|
|
apiUrl <- urlFromString apiUrlString
|
|
urlPort apiUrl
|
|
```
|
|
|
|
`do` in [[Either]] allows us to return early with errors.
|
|
|
|
```haskell
|
|
lookupEnv :: String -> Either String String
|
|
urlFromString :: String -> Either String URL
|
|
urlPort :: URL -> Maybe Int
|
|
|
|
getApiUrlPort = do
|
|
apiUrlString <- lookupEnv "API_URL"
|
|
apiUrl <- urlFromString apiUrlString
|
|
liftMaybe ("no port set in URL " <> apiUrlString)
|
|
$ urlPort apiUrl
|
|
```
|
|
|
|
> [!tip]- `liftMaybe`
|
|
> liftMaybe allows us to turn `Maybe a` into any `m a` as long as `m` supports [[MonadThrow|throwing]] errors.
|
|
>
|
|
> In the above example, if `urlPort` is `Nothing`, it will return `Left "no port set in ..."`.
|
|
>
|
|
> ```haskell
|
|
> liftMaybe :: forall m e a. MonadThrow e m => e -> Maybe a => m a
|
|
> ``` |