debuggingloggingvisual-studio-codefunctional-programmingpurescript

How to debug with PureScript?


Issue

Following is a minimal, contrived example:

read :: FilePath -> Aff String
read f = do
  log ("File: " <> f) -- (1)
  readTextFile UTF8 f -- (2)

I would like to do some debug logging in (1), before a potential error on (2) occurs. Executing following code in Spago REPL works for success cases so far:

$ spago repl
> launchAff_ $ read "test/data/tree/root.txt"
File: test/data/tree/root.txt
unit

Problem: If there is an error with (2) - file is directory here - , (1) seems to be not executed at all:

$ spago repl
> launchAff_ $ read "test/data/tree"
~/purescript-book/exercises/chapter9/.psci_modules/node_modules/Effect.Aff/foreign.js:532
                throw util.fromLeft(step);
                ^

[Error: EISDIR: illegal operation on a directory, read] {
  errno: -21,
  code: 'EISDIR',
  syscall: 'read'
}

The original problem is more complex including several layers of recursions (see E-Book exercise 3), where I need logging to debug above error.

How can I properly log regardless upcoming errors here?


Solution

  • First of all, the symptoms you observe do not mean that the first line doesn't execute. It does always execute, you're just not seeing output from it due to how console works in the PureScript REPL. The output gets swallowed. Not the only problem with REPL, sadly.

    You can verify that the first line is always executed by replacing log with throwError and observing that the error always gets thrown. Or, alternatively, you can make the first line modify a mutable cell instead of writing to the console, and then examine the cell's contents.

    Finally, this only happens in REPL. If you put that launchAff_ call inside main and run the program, you will always get the console output.


    Now to the actual question at hand: how to debug trace.

    Logging to console is fine if you can afford it, but there is a more elegant way: Debug.trace.

    This function has a hidden effect - i.e. its type says it's pure, but it really produces an effect when called. This little lie lets you use trace in a pure setting and thus debug pure code. No need for Effect! This is ok as long as used for debugging only, but don't put it in production code.

    The way it works is that it takes two parameters: the first one gets printed to console and the second one is a function to be called after printing, and the result of the whole thing is whatever that function returns. For example:

    calculateSomething :: Int -> Int -> Int
    calculateSomething x y =
        trace ("x = " <> show x) \_ ->
            x + y
    
    main :: Effect Unit
    main =
      log $ show $ calculateSomething 37 5
    
    >  npx spago run       
    'x = 37'               
    42                     
    

    The first parameter can be anything at all, not just a string. This lets you easily print a lot of stuff:

    calculateSomething :: Int -> Int -> Int
    calculateSomething x y =
        trace { x, y } \_ ->
            x + y
    
    >  npx spago run
    { x: 37, y: 5 }
    42
    

    Or, applying this to your code:

    read :: FilePath -> Aff String
    read f = trace ("File: " <> f) \_ -> do
      readTextFile UTF8 f
    

    But here's a subtle detail: this tracing happens as soon as you call read, even if the resulting Aff will never be actually executed. If you need tracing to happen on effectful execution, you'll need to make the trace call part of the action, and be careful not to make it the very first action in the sequence:

    read :: FilePath -> Aff String
    read f = do
      pure unit
      trace ("File: " <> f) \_ -> pure unit
      readTextFile UTF8 f
    

    It is, of course, a bit inconvenient to do this every time you need to trace in an effectful context, so there is a special function that does it for you - it's called traceM:

    read :: FilePath -> Aff String
    read f = do
      traceM ("File: " <> f)
      readTextFile UTF8 f
    

    If you look at its source code, you'll see that it does exactly what I did in the example above.


    The sad part is that trace won't help you in REPL when an exception happens, because it's still printing to console, so it'll still get swallowed for the same reasons.

    But even when it doesn't get swallowed, the output is a bit garbled, because trace actually outputs in color (to help you make it out among other output), and PureScript REPL has a complicated relationship with color:

    > calculateSomething 37 5
    ā†[32m'x = 37'ā†[39m
    42