purescript

Confused about do notation


I'm barely getting started with Purescript, and I hit a snag quite quickly. Consider the simple (and very artificial) example below:

module Main where

import Prelude

import Data.String.Common (joinWith)
import Data.String.Utils (lines)
import Effect (Effect)
import Effect.Console (log)

contents :: String
contents = "1 2\n3 4"

l :: Array String
l = lines contents

main :: Effect Unit
main = do
  log (joinWith "\n" l)

Simple stuff: create an array of strings using the lines function, join it back to a single string with joinWith, display it on the console. As expected, it compiles and outputs

1 2
3 4

Now, I rewrite this using "do notation", which seems to be equivalent to me:

module Main where

import Prelude

import Data.String.Common (joinWith)
import Data.String.Utils (lines)
import Effect (Effect)
import Effect.Console (log)

contents :: String
contents = "1 2\n3 4"

main = do
  l <- lines contents
  log (joinWith "\n" l)

I've just moved l inside the do block, so in my mind, it should be equivalent... however, the Purescript compiler disagrees:

Could not match type
          
    String
          
  with type
                
    Array String
                

while checking that type String
  is at least as general as type Array String
while checking that expression l
  has type Array String
in value declaration main

If I read this error message correctly, it implies that now l has type String, while in the original code (where l is outside the do block), it has type Array String. Yet, in both cases, it receives the result of split, which does have type Array String according to Pursuit.

I don't understand what's different in both cases. Why is l's type different in these two cases, and how could I make the second code example work?


Solution

  • The "left arrow" <- thingy isn't "variable assignment" as it seems like you're assuming.

    Instead, the <- thingy is something called "monadic bind", which basically means "run the computation on the right and give its result the name on the left". But the important bit is that "computation on the right" is not just any expression. It has to be a computation in the same monad in which the line appears.

    So in your case, since main :: Effect Unit, the do notation is "running" in the Effect monad, and therefore, the thing on the right of the <- arrow has to also be an Effect. That effect would be executed and the result of it would be named l.

    But the expression lines contents is not an Effect! It's an Array. So the whole thing doesn't compute.

    To give a name to a value, which is not a computation in the same monad, use let instead of the <- arrow:

    main :: Effect Unit
    main = do
      let l = lines content
      log (joinWith "\n" l)
    

    But you complicated things a bit by removing the main :: Effect Unit type signature, in what I can only assume to be an attempt to shoehorn the bloody thing into working. This was a mistake, because it moved the error and made it less obvious.

    The thing is, Effect is not the only thing that works with the do notation. Not the only monad, to use a fancypants term. There are many of them. Too many, if you ask me.

    In particular, Array is also a monad. Surprise! So the do notation also works with arrays. Something like:

    squares :: Array Int
    squares = do
      n <- 1..10
      pure (n * n)
    

    And so, since your main didn't have a type signature to tell the compiler that it's supposed to be an Effect, and the first line of it was trying to <- bind an expression of type Array String, the compiler decided that you meant the whole thing to run in the Array monad, and happily complied.

    And since the whole thing is now in the Array monad, and the expression to the right of the <- arrow is of type Array String, that makes the "result" of that just String, and so this is what the type of l must be.

    And from there you get an error with joinWith, because it expects a parameter of type Array String, but you gave it just String.

    Lesson learned: use type signatures to tell the compiler what you expect things to be. Otherwise it will come up with its own conclusions.


    You may be also interested in reading this explanation: Can someone clarify monads / computation expressions and their syntax, in F#