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