15

Given:

λ: let f = putStrLn "foo" in 42
42

What is f's type? Why does "foo" not get printed before showing the result of 42?

Lastly, why doesn't the following work?

λ: :t f

<interactive>:1:1: Not in scope: ‘f’
Kevin Meredith
  • 41,036
  • 63
  • 209
  • 384
  • You might want to read about [`let` vs `<-`](http://stackoverflow.com/q/28624408/3234959). It is not the exact issue you are having now, but it might help. – chi May 27 '16 at 08:05
  • Read [Why can't I force an IO action with seq?](http://stackoverflow.com/q/20324446/510937) for a bit more info on evaluating vs executing IO actions. – Bakuriu May 27 '16 at 09:56

4 Answers4

13

What is f's type?

As you have correctly identified, it is IO () which can be thought of as an IO action that returns nothing useful (())

Why does "foo" not get printed before showing the result of 42?

Haskell is lazily evaluated, but even seq is not enough in this case. An IO action will only be performed in the REPL if the expression returns the IO action. An IO action will only be performed in a program if it's returned by main. However, there are ways to get around this limitation.

Lastly, why doesn't the following work?

Haskell's let names a value within the scope of an expression, so after the expression has been evaluated f goes out of scope.

Vaibhav Sagar
  • 2,208
  • 15
  • 21
10

let f = ... simply defines f, and does not "run" anything. It is vaguely similar to a definition of a new function in imperative programming.

Your full code let f = putStrLn "foo" in 42 could be loosely translated to

{
  function f() {
     print("foo");
  }
  return 42;
}

You wouldn't expect the above to print anything, right?

By comparison, let f = putStrLn "foo" in do f; f; return 42 is similar to

{
  function f() {
     print("foo");
  }
  f();
  f();
  return 42;
}

The correspondence is not perfect, but hopefully you get the idea.

chi
  • 111,837
  • 3
  • 133
  • 218
7

f will be of type IO ().

"foo" is not printed because f is not 'binded' to real world. (I can't say this is a friendly explanation. If this sounds nonsense, you may want to refer some tutorial to catch the idea of Monad and lazy evaluation).

let name = value in (scope) makes the value available in, but not out of the scope, so :t won't find it in ghci's top level scope.

let without in makes it available to :t (this code is only valid in ghci):

> let f = putStrLn "foo"
> :t f
f :: IO ()
Jokester
  • 5,501
  • 3
  • 31
  • 39
4

There are two things going on here.

First, consider

let x = sum [1..1000000] in 42

Haskell is lazy. Since we don't actually do anything with x, it is never computed. (Which is just as well, because it would be mildly slow.) Indeed, if you compile this, the compiler will see that x is never used, and delete it (i.e., not generate any compiled code for it).

Second, calling putStrLn does not actually print anything. Rather, it returns IO (), which you can think of as a kind of "I/O command object". Merely having a command object is different from executing the it. By design, the only way to "execute" an I/O command object is to return it from main. At least, it is in a complete program; GHCi has the helpful feature that if you enter an expression that returns an I/O command object, GHCi will execute it for you.

Your expression returns 42; again, f isn't used, so it doesn't do anything.

As chi rightly points out, it's a bit like declaring a local (zero-argument) function but never calling it. You wouldn't expect to see any output.

You can also do something like

actions = [print 5, print 6, print 7, print 8]

This creates a list of I/O command objects. But, again, it does not execute any of them.

Typically when you write a function that does I/O, it's a do-block that chains everything into one giant I/O command object and returns it to the caller. In that case, you don't really need to understand or thing about this distinction between defining a command object and executing it. But the distinction is still there.

It's perhaps easier to see this with a monad that has an explicit run-function. For example, runST takes an ST command object, runs it, and gives you back the answer. But (say) newSTVar, by itself, does nothing but construct an ST command; you have to runST that before anything actually "happens".

MathematicalOrchid
  • 61,854
  • 19
  • 123
  • 220