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 URLAPI_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 turnMaybe a
into anym a
as long asm
supports MonadThrow errors.In the above example, if
urlPort
isNothing
, it will returnLeft "no port set in ..."
.liftMaybe :: forall m e a. MonadThrow e m => e -> Maybe a => m a