F# – Another Disappointment: Lazy Evaluation

After exploring “lazy” keyword in F#, I was up for some disappointments:

<DISCLAIMER> Fast forward to the year 2012: people from Microsoft’s “try F#” team pointed out that I got some things wrong here, and they probably have a point. So, take whatever follows with a grain of salt. Also, take a look at the comments.</DISCLAIMER>

* lazy (bla) cannot be transparently used in expressions instead of bla
* Results of lazy evaluation are not cached
* Parameters of lazy functions are evaluated eagerly

Let me explain each of these points.

Lazy values are not transparent

Lazy values such as lazy x have a template type Lazy<'a>, or in C# terms Lazy<T>. This type does not inherit operations of T. If you have two lazy integers, you cannot lazily add them via ‘+’: (lazy 2) + (lazy 3) won’t work. To lazily add two lazy integers you will have to write a special function along the lines of:

let lazy_add x y =
 lazy( Lazy.force(x) + Lazy.force(y) )

You must explicitly call Lazy.force() or similar function to obtain the value of the lazy expression.

This all makes laziness in F# very explicit. You manually say when things are lazy, you manually say when you want to force the calculation, and you cannot manipulate lazy objects as if they were regular objects.

Lazy Evaluation Results Are Not Cached

In purely functional languages like Haskell, lazy evaluation results are cached my default. If the system already calculated the value of foo(42), it will not calculate it again. This makes sense, since it is assumed that function don’t have side effects.

F# does not cache lazy evaluated values. This can be demonstrated by the following code:

#light
open System

let input x =
 lazy
 (
  printf "Please enter value for "
  print_any x
  printf ": "
  Console.ReadLine()
 )

let a = (input 42) |> Lazy.force
let b = (input 42) |> Lazy.force

The code requests the input twice. If the value of input 42 were cached, it would request the input only once. This is exactly what happens if we cache the value manually:


let cache = input 42
let a = cache |> Lazy.force
let b = cache |> Lazy.force

Caching the values manually requires writing code, and it may lead to threading issues. Consider this memoization sample from “Expert F#” book:

let fibFast =
 let t = new System.Collections.Generic.Dictionary<int,int>()
 let rec fibCached n =
  if t.ContainsKey(n) then t.[n]
  else if n <= 2 then 1
  else let res = fibCached(n-1) + fibCached(n-2)
    t.Add(n,res)
    res
 fun n -> fibCached n

This example uses a (local) dictionary, which would require locks if we wanted to parallelize fib_cached.

Parameters of Lazy Functions are Evaluated Eagerly

Consider:

let lazy_add a b =
 lazy (a+b)

let use_lazy_add x =
 lazy_add func1(x) func2(x)

The calls to func1() and func2() will be evaluated eagerly. Only the addition of the results will be lazy. This is not very bad per se, but it hinders functional decomposition. If I have nested function calls, “laziness” must be applied on the very top level. Consider:

let f1 x = ...
let f2 x = ...

// use f1 lazily
let r1 = f1 42
let r2 = f2 18

let lazy_add x y =
  lazy( x+y )

let add_result = lazy_add 10 20
let add_result2 = lazy_add r1 r2 // oops! this won't work
let add_result3 = lazy( r1+r2 ) // that won't work either
let add_reslut4 = lazy_add Lazy.force(r1) Lazy.force(r2) // will eagerly call f1, f2
let add_result5 = lazy (Lazy.force(r1) + Lazy.force(r2)) // this will do what we want
Posted in

3 Comments


  1. What you call “transparent laziness” from Haskell requires purity in order to be deterministic and F# (like all MLs) is not a pure language. There are many important benefits to remaining impure (e.g. easy performance and predictable memory consumption).

    Contrary to your claim, forced thunks are memoized in F# exactly as they should be. You have defined “input” as a function with an unused “x” argument when it should be a lazy value. Removing your superfluous “x” argument fixes the problem and the lazy value is memoized correctly.

    Your statements about the memoize combinator are also misleading. Firstly, that combinator has nothing to do with laziness. Secondly, that implementation requires locks to be thread safe in any language because it uses a hash table that is mutated in place.

    Reply

  2. I am not 100% sure what you are talking about, but the “input” function does use its argument x in the print_any statement.

    Reply

  3. Wrapping your lazy expression in an impure function as you have done forces it to be regenerated every time the function is invoked. The code cannot work as you imply it should. Moreover, your comments are about function evaluation semantics and have nothing to do with laziness.

    Reply

Leave a Reply to Jon Harrop Cancel reply

Your email address will not be published. Required fields are marked *