To practice my Haskell I decided to write a little JSON parser. In the main file I call the different parts of my parser, print the results so I have more information for debugging and then write the parsed JSON back to a file:
{-# LANGUAGE OverloadedStrings #-}
module Main where
import qualified Lexer as L
import qualified Parser as P
import qualified Printer as PR
import qualified Data.Text.Lazy.IO as TIO
main :: IO ()
main = do
input <- TIO.readFile "input.json"
case L.tokenize input of
Nothing -> putStrLn "Syntax error!"
Just tokens -> do
print tokens
case P.parse tokens of
Nothing -> putStrLn "Parse error!"
Just parsedValue -> do
print parsedValue
TIO.writeFile "output.json" $ PR.toText parsedValue
Unfortunately, I get this ugly nested code where I use multiple do-expressions inside of each other. In my understanding, one of the main reasons to use monads and do-notation is to avoid this kind of code-nesting. I could for example use the Maybe monad to evaluate the different parsing steps (lexing, parsing) without the need to check the success of each step individually. Sadly this is not possible in this example because I need to use functions such as print and writeFile that require an IO monad alternately with functions that require a Maybe monad.
How could I refactor this code to be less nested and to include less do-expressions? Or more generally, how can I write clean code that contains calls to functions of different monads? Is it somehow possible to "mix" two monads in the same do-expression, a bit like this?
main :: IO ()
main = do
input <- TIO.readFile "input.json"
tokens <- L.tokenize input
print tokens
parsedValue <- P.parse tokens
print parsedValue
TIO.writeFile "output.json" $ PR.toText parsedValue
First of all, good intuition about
donotation! In this case, you want to combine theEither Stringmonad together with theIOmonad. The result will be a new monad in which you will get a flatdo-block. (Note you don't wantMaybe, sinceMaybedoesn't let you record error information.) The combined monad ofEither StringandIOis calledExceptT String IO, whereExceptTis the following type defined in thetransformerspackage (which should come with any installation of GHC).You'll want to use it together with a function like
that annotates the "uninformative" failure of
Maybewith a given error message, as well as a function likewhich "handles" the
ExceptT Stringeffect and leaves behind justIO. You will also need the function (defined intransformers)to fit existing
IOactions into this new monad.An alternative solution is to just use a function like
This leverages the fact that
IOalready has a built-in exception mechanism. However, the difference (I consider it an issue) with this is that the errors that can arise from an action are not clear in its type anymore, and therefore well-structured error handling is not enforced. (E.g. note that I'm not forced to handle my exceptions with a function likeprintingErroranymore. The exceptions just bubble up pastmainand get handled by the runtime system.)(NB: I haven't tested anything in this answer. Sorry if there are errors.)