Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Practical differences between Lazy<_> and thunk (fun () -> ...)

In the F# core library there are higher-order functions that take a thunk (fun () -> ...), but could also conceptually take a Lazy<_>, such as Option.defaultWith. F# has good syntactical support for Lazy<_> through the lazy keyword, but I can't think of any function in the F# core library that takes Lazy<_> instead of a thunk. I would guess this is because it's "more FP" to use thunks than the .NET-specific Lazy<_> type.

But apart from being vaguely "less FP": If the value is needed at most 1 time, what are the practical considerations of using Lazy<_> instead of thunks? Are there, for example, performance differences (CPU and/or allocations/memory)? Other concerns? What kind of situations are better resolved with Lazy<_> instead of thunks?

like image 661
cmeeren Avatar asked Dec 12 '18 12:12

cmeeren


1 Answers

Being a separate object which wraps a thunk, provides synchronization and holds the result reference, Lazy<_> has some additional overhead compared to a simple thunk.

If you know you're only going to evaluate the thunk (at most) once, I don't think there's a reason why you wouldn't use a function if you can. You can wrap a call to Lazy<_>'s Value in another function, but in this case it has no benefit that I know of.

One scenario I remember where Lazy<_> was useful was when we had two separate feature flags in a product which both potentially required initialization of Orleankka actor system if enabled - expensive operation which should only be done once per application startup.

So our options were either a lot of nested ifs, mutable options, or this:

let actorSystem = lazy initializeActorSystem ()

if feature1Enabled then
    let as = actorSystem.Value
    ...

if feature2Enabled then
    let as = actorSystem.Value
    ...

Unlike a thunk, Lazy<_> also allows you to check if the thunk was evaluated. From the same exaple:

if actorSystem.IsValueCreated then
    actorSystem.Value.Dispose()

Other cases are in general when you aren't sure you would evaluate the thunk at most once, especially if it can happen concurrently. I think we also have use case like that for reading some external configuration when not provided locally to several components that are being started concurrently - Lazy<_> ensures we make the remote call only once (if needed at all) regardless of which component first finds out it needs it.

like image 169
Honza Brestan Avatar answered Nov 07 '22 02:11

Honza Brestan