F# Lazy Evaluation vs Non-Lazy

2020-08-17 12:39发布

问题:

I'm just beginning F# so please be kind if this is basic.

I've read that a function marked lazy is evaluated only once and then cached. For example:

let lazyFunc = lazy (1 + 1)
let theValue = Lazy.force lazyFunc

Compared to this version which would actually run each time it's called:

let eagerFunc = (1 + 1)
let theValue = eagerFunc

Based on that, should all functions be made lazy? When would you not want to? This is coming from material in the book "Beginning F#".

回答1:

First of all, it may be helpful to note that none of the things you have defined is a function - eagerFunc and theValue are values of type int and lazyFunc is a value of type Lazy<int>. Given

let lazyTwo = lazy (1 + 1)

and

let eagerTwo = 1 + 1

the expression 1 + 1 will not be evaluated more than once no matter how many times you use eagerTwo. The difference is that 1 + 1 will be evaluated exactly once when defining eagerTwo, but will be evaluated at most once when lazyTwo is used (it will be evaluated the first time that the Value property is accessed, and then cached so that further uses of Value do not need to recalculated it). If lazyTwo's Value is never accessed, then its body 1 + 1 will never be evaluated.

Typically, you won't see much benefit to using lazy values in a strict language like F#. They add a small amount of overhead since accessing the Value property requires checking whether the value has already been calculated. They might save you a bit of calculation if you have something like let lazyValue = lazy someVeryExpensiveCalculationThatMightNotBeNeeded(), since the expensive calculation will only take place if the value is actually used. They can also make some algorithms terminate which otherwise would not, but this is not a major issue in F#. For instance:

// throws an exception if x = 0.0
let eagerDivision x =
    let oneOverX = 1.0 / x
    if x = 0.0 then
        printfn "Tried to divide by zero" // too late, this line is never reached
    else
        printfn "One over x is: %f" oneOverX

// succeeds even if x = 0.0, since the quotient is lazily evaluated
let lazyDivision x =
    let oneOverX = lazy (1.0 / x)
    if x = 0.0 then
        printfn "Tried to divide by zero"
    else
        printfn "One over x is: %f" oneOverX.Value


回答2:

If function executions have side-effects and it is important to see the side-effects each time the function is called (say it wraps an I/O function) you would not want it to be lazy.

There are also functions that are so trivial that executing them each time is faster than caching the value--



回答3:

let eagerFunc = (1 + 1) is a let binding, and will only execute once. let eagerFunc() = (1 + 1) is a function accepting unit (nothing) and returning an int. It will execute each time it's called. In a sense, every function is lazy, that is, it only executes when called. However, the lazy keyword (and System.Lazy, which it returns) will execute the expression/function given to it at most once. Subsequent calls to the Value property will return the cached result. This is useful when computation of the value is expensive.

Many functions will not be suitable for use with lazy because they are either non-deterministic (may return a different result with each invocation) or parameterized. Of course, it's possible to use a fully-applied (a value is supplied for each parameter) version of such functions, but generally the variability is desired.