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