arliss_obsidian/fp/Language/Expressions/do notation.md
Orion Kindel 3ee2a9bbbd
update
2024-09-24 17:52:08 -05:00

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
> ```