Let-in vs let which should I use?

Hello everyone first time poster here.
I am going through the purescript by example book and one of the proposed solution to this exercise raised the question about let-in vs let (with out in) when do I use one or the other?
Why this example was not written with let-in?

recurseFiles :: FilePath -> Aff (Array FilePath)
recurseFiles file = do
  contents <- readTextFile UTF8 file
  case contents of
    "" -> pure [ file ]
    c -> do
      let
        dir = Path.dirname file

        files = split (Pattern "\n") contents

        filesFromRoot = map (\f -> Path.concat [ dir, f ]) files
      arrarr <- parTraverse recurseFiles filesFromRoot
      pure $ file : concat arrarr

Thanks for taking your time to answer this silly question

4 Likes

I don’t think it’s a silly question. It’s pretty confusing to start with! Simply put, let without in is used only in do blocks, and is transformed to a let ... in expression by the compiler as part of its desugaring of do blocks. (If you don’t know already, do blocks are just a convenient way of writing expressions with monadic “stuff” going on.)

The description of the syntax for do blocks is here and the description for “normal” let is here. The example for the do block there might make it a bit clearer what’s going on.

7 Likes

There’s also my repo that covers “do” and “ado” notation: https://github.com/JordanMartinez/purescript-jordans-reference/tree/latestRelease/11-Syntax/05-Prelude-Syntax/src

1 Like

Ok now I know how it is used. Now when should I use on or the other?
What is more idiomatic, performant or has an edge over the other:

recurseFiles :: FilePath -> Aff (Array FilePath)
recurseFiles file = do
  contents <- readTextFile UTF8 file
  case contents of
    "" -> pure [ file ]
    c -> do
      let
        dir = Path.dirname file

        files = split (Pattern "\n") contents

        filesFromRoot = map (\f -> Path.concat [ dir, f ]) files
      arrarr <- parTraverse recurseFiles filesFromRoot
      pure $ file : concat arrarr
recurseFiles :: FilePath -> Aff (Array FilePath)
recurseFiles file = do
  contents <- readTextFile UTF8 file
  case contents of
    "" -> pure [ file ]
    c -> let
        dir = Path.dirname file

        files = split (Pattern "\n") contents

        filesFromRoot = map (\f -> Path.concat [ dir, f ]) files
      in do
        arrarr <- parTraverse recurseFiles filesFromRoot
        pure $ file : concat arrarr

Or even the where version

recurseFiles :: FilePath -> Aff (Array FilePath)
recurseFiles file = do
  contents <- readTextFile UTF8 file
  case contents of
    "" -> pure [ file ]
    c -> do
      arrarr <- parTraverse recurseFiles filesFromRoot
      pure $ file : concat arrarr
      where
        dir = Path.dirname file
        files = split (Pattern "\n") contents
        filesFromRoot = map (\f -> Path.concat [ dir, f ]) files

There isn’t a difference between any of those. They all end up the same. I personally never use let/in. At Awake we almost always use do/let, even for “pure” expressions, while also using where if it reads better. Sometimes things read better when the nitty-gritty is put last (no need to worry about the details). where doesn’t work with lambdas though, and so if you want uniformity, then you should use let/in or do/let, and I think let/in looks terrible.

2 Likes

I originally used let in until I learned that I could use do let for even non-monadic code. I think using do let is better than let in for at least one reason. If I ever needed to migrate non-monadic code to monadic code, I only need to change the type signature and the implementation, not the let part.

4 Likes

I did not know about do/let in pure function, I think you should add it to jordan reference

but I hate that purty does not allow let in single line

It’s kind of hard to describe do/let in a reasonable location in my repo. I have tried to ensure that every page never assumes you know more than what I’ve already described in the pages before that one.

I currently describe the do/let part in the Prelude Syntax folder. However, the issue of do/let is that one needs to understand monads before one describes it to readers. Else, why not just use let/in? So, if most people just read through the Basic Syntax, where I cover let/in, they might not read the Prelude Syntax.

Moreover, this is a syntactical choice, so it doesn’t fit in the “Design Patterns” part of the work.

Perhaps I need to state do/let more explicitly in the Prelude Syntax folder?

1 Like

oh I’m not finished reading your reference yet, so I didn’t know that you mentioned do/let anywhere.

1 Like

Wow, I never knew that do/let could be used for non-monadic code too. Neat!

I’m also surprised to see that where binds to do! But only sometimes? This seems to work:

x :: Int
x = case true of
  false -> 0
  true -> do a + b
    where a = 1
          b = 2

But not this:

x :: Int
x = (
  do a + b
    where a = 1
          b = 2
)

Is it just for do-blocks directly after =, ->, and in?

you can use where in any case branch! (you do not need the do block)

1 Like

Grammatically, where is tied to the right-hand-side of = and case ->. It is not an arbitrary infix operator.

1 Like