Haskell "hello world" with let

Several related things really bothered me about Haskell's symmetry when I began learning Haskell, so I'd like to give other novices a heads up. This brief post explains the essentials, including let.

First, a straightforward program:

intro txt =
   let
      welcome = "Welcome! Today we will "
   in
      welcome ++ txt

main =
   let
      plan = "talk about symmetry!"
   in
      do
         let hello = intro plan
         putStrLn hello

It can be saved in a file, hello.hs, and run like so:

$ runhaskell hello.hs
Welcome! Today we will talk about symmetry!

The first thing you notice about the let keywords, especially if you have worked with F#, first, is that there are none at the top level!

This is not because Haskell is a hideously asymmetric language: it is because this symmetry is simply implied. Each top level function declaration looks, actually, the same way it would if all of them were wrapped in let and in keywords, with main called below that. Imagine it was like this:

let
   intro txt =
      let
         welcome = "Welcome! Today we will "
      in
         welcome ++ txt

   main =
      let
         plan = "talk about symmetry!"
      in
         do
            let hello = intro plan
            putStrLn hello
in
   main

The second thing you'll notice about these let keywords is that after the let hello = ... line, there is no matching in keyword! You may pause a moment to ask whether these pesky in keywords are even needed at all. In fact, you will rush to test this, by launching the REPL, ghci:

$ ghci
Prelude> let hello = "look at me swimming!"
Prelude> putStrLn hello
look at me swimming!

The glow lasts for a moment, and then you try removing the in after the definitions of welcome and plan and you find rejection!! A parse error?

$ runhaskell hello.hs

hello.hs:7:1: parse error (possibly incorrect indentation)

The secret is two-fold. First, the let hello = ... is special, because it is in a do block. (Later, you'll learn how this special do block sub-language works.) Second, and most shockingly, to me: ghci provides a REPL wrapped in an implicit do.

To make it easy to copy and paste lines, employing this feature, you can shuffle things around in the source file, like so:

main =
   do
      let intro txt = "Welcome! Today we will " ++ txt
      let plan      = "talk about symmetry!"
      let hello     = intro plan
      putStrLn hello

Then you can run it as before, or copy-paste only a portion into ghci, like this:

$ ghci
Prelude>       let intro txt = "Welcome! Today we will " ++ txt
Prelude>       let plan      = "talk about symmetry!"
Prelude>       let hello     = intro plan
Prelude>       putStrLn hello
Welcome! Today we will talk about symmetry!

There is, however, a big shortcut. To load all the code into ghci without even stressing at all about copy-paste errors, simply load the file itself using a ghci command.

The :load command lets us load a file or (module) into the runtime, so we can then call its functions. (And if we had command line arguments to the program, we can even use the :main command, with colon, to test them.

$ ghci
Prelude> :load hello.hs
*Main>   intro "play with hula hoops."
"Welcome! Today we will play with hula hoops."

Cheers,
Kevin