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

4.5 KiB

Do notation is sugar for Bind >>=, allowing imperative blocks that use bind as the driver of the "and-then" behavior for the specific type being returned.

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:

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.

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:

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:

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:

do
  a <- Just 1
  let b =
    a + 1
  Just b
[ERROR 1/1 ErrorParsingModule] ...

 4    a + 1
                

 Unable to parse module:
 Unexpected or mismatched indentation

This happens because oneline let bindings

do
  let a = b
  ..

are identical to

do
  let
    a = b
  ..

So when you break after the equals and only indent once:

do
  let a =
    b
  ..

what you're actually writing is

do
  let
    a =
    b
  ..

eval and ignore

When an expression returns m Unit, you don't have to write an arrow:

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

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:

-- block immediately resolves with `Just ""`
do
  Just ""
-- block resolves with `Just foo`
do
  foo <- getFoo
  Just foo
-- 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
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.

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 errors.

In the above example, if urlPort is Nothing, it will return Left "no port set in ...".

liftMaybe :: forall m e a. MonadThrow e m => e -> Maybe a => m a