Typeclass is great to reduce boilerplate.
In Racket, I define macros to chain computation. For example, in my latest automation, I define a chain operation to chain a series of git commands.
(result~> : (Result String String)
(sh "git add -A -N" (Some pwd))
(sh "git add -u" (Some pwd))
(sh "git status -s" (Some pwd))
(let ([text (string-trim _)])
(if (equal? text "")
NO-DEST-CHANGES-RESULT
(Ok text)))
(sh (format "git commit -m '~a'" _) (Some pwd))
(sh "git push origin master" (Some pwd)))))
My result~> macro in Typed Racket.
There are three cool features of the result~>:
- The operations are short circuit by any one of the expressions that returns (Error stderr)
- If one operation succeeds, the next operation can retrieve the previous stdout using _
- It’s type safe; all expression needs to return Result type within the body of result~>
Well, in Haskell the do-notation seems great for reducing boilerplate within context. What’s cooler than the Racket macro is that I can write different things for different context, and from the syntax level, they look exactly the same. Below I implement a CommandM to carry IO computation, and any failures short-circuit the entire computation.
newtype CommandM a = CommandM (IO (Either String a))
upload :: String -> CommandM String
upload pwd = do
_ <- sh "git" ["add", "-A", "-N"] (Just pwd)
_ <- sh "git" ["add", "-u"] (Just pwd)
text <- sh "git" ["status", "-s"] (Just pwd)
msg <- if text == ""
then CommandM $ pure $ Left "No changes"
else pure text
_ <- sh "git" ["commit", "-m", msg] (Just pwd)
sh "git" ["push", "origin", "master"] (Just pwd)