-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Lazy.map #10097
Lazy.map #10097
Changes from all commits
812f546
09c940f
9ee6e6d
3d93b0b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -57,7 +57,6 @@ type 'a t = 'a CamlinternalLazy.t | |
|
||
exception Undefined | ||
|
||
(* val force : 'a t -> 'a *) | ||
external force : 'a t -> 'a = "%lazy_force" | ||
(** [force x] forces the suspension [x] and returns its result. | ||
If [x] has already been forced, [Lazy.force x] returns the | ||
|
@@ -67,36 +66,75 @@ external force : 'a t -> 'a = "%lazy_force" | |
recursively. | ||
*) | ||
|
||
val force_val : 'a t -> 'a | ||
(** [force_val x] forces the suspension [x] and returns its | ||
result. If [x] has already been forced, [force_val x] | ||
returns the same value again without recomputing it. | ||
(** {1 Iterators} *) | ||
|
||
If the computation of [x] raises an exception, it is unspecified | ||
whether [force_val x] raises the same exception or {!Undefined}. | ||
@raise Undefined if the forcing of [x] tries to force [x] itself | ||
recursively. | ||
val map : ('a -> 'b) -> 'a t -> 'b t | ||
(** [map f x] returns a suspension that, when forced, | ||
forces [x] and applies [f] to its value. | ||
|
||
It is equivalent to [lazy (f (Lazy.force x))]. | ||
|
||
@since 4.13.0 | ||
*) | ||
|
||
(** {1 Reasoning on already-forced suspensions} *) | ||
gasche marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
val is_val : 'a t -> bool | ||
(** [is_val x] returns [true] if [x] has already been forced and | ||
did not raise an exception. | ||
@since 4.00.0 *) | ||
|
||
val from_val : 'a -> 'a t | ||
(** [from_val v] evaluates [v] first (as any function would) and returns | ||
an already-forced suspension of its result. | ||
It is the same as [let x = v in lazy x], but uses dynamic tests | ||
to optimize suspension creation in some cases. | ||
@since 4.00.0 *) | ||
|
||
val map_val : ('a -> 'b) -> 'a t -> 'b t | ||
(** [map_val f x] applies [f] directly if [x] is already forced, | ||
otherwise it behaves as [map f x]. | ||
|
||
When [x] is already forced, this behavior saves the construction of | ||
a suspension, but on the other hand it performs more work eagerly | ||
that may not be useful if you never force the function result. | ||
gasche marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
If [f] raises an exception, it will be raised immediately when | ||
[is_val x], or raised only when forcing the thunk otherwise. | ||
|
||
If [map_val f x] does not raise an exception, then | ||
[is_val (map_val f x)] is equal to [is_val x]. | ||
|
||
@since 4.13.0 *) | ||
|
||
|
||
(** {1 Advanced} | ||
|
||
The following definitions are for advanced uses only; they require | ||
familiary with the lazy compilation scheme to be used appropriately. *) | ||
|
||
val from_fun : (unit -> 'a) -> 'a t | ||
(** [from_fun f] is the same as [lazy (f ())] but slightly more efficient. | ||
|
||
[from_fun] should only be used if the function [f] is already defined. | ||
It should only be used if the function [f] is already defined. | ||
In particular it is always less efficient to write | ||
[from_fun (fun () -> expr)] than [lazy expr]. | ||
|
||
@since 4.00.0 *) | ||
|
||
val from_val : 'a -> 'a t | ||
(** [from_val v] returns an already-forced suspension of [v]. | ||
This is for special purposes only and should not be confused with | ||
[lazy (v)]. | ||
@since 4.00.0 *) | ||
val force_val : 'a t -> 'a | ||
(** [force_val x] forces the suspension [x] and returns its | ||
result. If [x] has already been forced, [force_val x] | ||
returns the same value again without recomputing it. | ||
|
||
val is_val : 'a t -> bool | ||
(** [is_val x] returns [true] if [x] has already been forced and | ||
did not raise an exception. | ||
@since 4.00.0 *) | ||
If the computation of [x] raises an exception, it is unspecified | ||
whether [force_val x] raises the same exception or {!Undefined}. | ||
@raise Undefined if the forcing of [x] tries to force [x] itself | ||
recursively. | ||
Comment on lines
+130
to
+133
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This docstring notes some unspecified behaviour, and then specifies it in the next sentence. It seems fairly clear under what circumstances force_val will raise There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that the two sentences are talking about different things: (1) what happens if the suspended computation raises an exception (say In any case, this is unrelated to the change in this PR, so I would propose to deal with any potential change proposals here in a separate issue/PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah yes, that's clearer. I guess I don't understand under what circumstances it's ever safe to raise an exception from within something forced by this function then, as it always has the opportunity to gobble the exception and raise a Undefined instead. Give that, how can an exception ever safely be thrown? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (There's the case of a non-recursive function for which you can inspect all possible exceptions, but then we already know that it can never raised There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't tell about the original intent, but |
||
*) | ||
|
||
|
||
(** {1 Deprecated} *) | ||
|
||
val lazy_from_fun : (unit -> 'a) -> 'a t | ||
[@@ocaml.deprecated "Use Lazy.from_fun instead."] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
(* TEST | ||
* expect | ||
*) | ||
|
||
(* expect-tests currently do not collect I/O, | ||
so we emulate I/O by collecting output in a "log" *) | ||
let logger () = | ||
let log = ref [] in | ||
let show_log v = List.rev !log, v in | ||
let log v = log := v :: !log in | ||
log, show_log | ||
[%%expect{| | ||
val logger : unit -> ('a -> unit) * ('b -> 'a list * 'b) = <fun> | ||
|}] | ||
|
||
let _ = | ||
let log, show_log = logger () in | ||
let x = lazy (log "x"; 41) in | ||
let y = | ||
log "map"; | ||
Lazy.map (fun n -> log "y"; n+1) x in | ||
log "force y"; | ||
show_log (Lazy.force y) | ||
;; | ||
[%%expect{| | ||
- : string list * int = (["map"; "force y"; "x"; "y"], 42) | ||
|}] | ||
|
||
let _ = | ||
let log, show_log = logger () in | ||
let x = lazy (log "x"; 41) in | ||
let y = | ||
log "map_val"; | ||
Lazy.map_val (fun n -> log "y"; n+1) x in | ||
assert (not (Lazy.is_val y)); | ||
log "force y"; | ||
show_log (Lazy.force y) | ||
;; | ||
[%%expect{| | ||
- : string list * int = (["map_val"; "force y"; "x"; "y"], 42) | ||
|}] | ||
|
||
let _ = | ||
let log, show_log = logger () in | ||
let x = lazy (log "x"; 41) in | ||
log "force x"; | ||
let () = ignore (Lazy.force x) in | ||
let y = | ||
log "map_val"; | ||
Lazy.map_val (fun n -> log "y"; n+1) x in | ||
assert (Lazy.is_val y); | ||
log "y is val"; | ||
show_log (Lazy.force y) | ||
;; | ||
[%%expect{| | ||
- : string list * int = (["force x"; "x"; "map_val"; "y"; "y is val"], 42) | ||
|}] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another useful variant would apply
f
eagerly if the suspension had already been forced (particularly useful whenf
is very cheap, to avoid building a useless closure and possibly keepingx
alive for too long). One could even argue that this function would be a more naturalmap
over lazy values, seen as (mutable) sum types with two constructors (forced value or lazy thunk). Should we provide both variants (or just one function with an optional flag)? I'm not strongly asking for it, just wanted to raise the point in case this affects the naming choice.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this optimization makes sense (but in the same function, no need for a separate function or an extra argument). The docstring should be amended to indicate that
f
could be executed right away if the lazy has already been forced.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an interesting optimization but its semantics is weird, so it should definitely not be the default behavior. By "weird semantics", I mean it's neither lazy nor strict. The lazy behavior is
as in this PR, and the strict behavior is
In by-the-book functional programming, there is no other option! The optimized map proposed by @alainfrisch can only be defined if primitives like
Lazy.is_val
are provided, which again is not by-the-book functional programming.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @xavierleroy that this definitely shouldn't be the default. If
f
is expensive and the resulting lazy is not forced, then the optimisation can do unbounded unnecessary work, precisely what the user is usingLazy
to avoid.This is indeed the natural map over sums with two constructors, but I see that as an implementation detail of Lazy rather than a spec. The natural map over computations-evaluated-only-if-forced is @gasche's, because it has:
where
==
is observational equivalence (assuming you don't get to observe with the "advanced" parts of Lazy). By constrast, the optimised sometimes-eager version:makes it observable whether the compiler has performed constant-folding.