Option.fold with init taking a thunk

final def fold[B](ifEmpty: => B)(f: (A) => B): B

Coming from the scala land, I’m used to seeing the Option.fold take a thunk as argument that only gets executed if the Option is None.

I was hoping there would be something similar in OCaml but it appears there isn’t. Am I wrong?

The workaround that I came up with to emulate similar behavior is OCaml is wrap it with Lazy.t. Is there a better way than this? I’d like to avoid match statement if possible. Thanks!

  let init = Lazy.from_fun (fun _ -> linspace (t, x_max) |> Tensor.unsqueeze ~dim:1) in
  let handle_cdim _ dim =
    let splits = Tensor.split t ~split_size:1 ~dim in
    let x_maxs = List.map splits ~f:(Fn.compose Tensor.to_float0_exn Tensor.maximum) in
    let ts = List.zip_exn splits x_maxs |> List.map ~f:linspace in
    Lazy.from_val (Tensor.stack ts ~dim:1)
  in
  let result = Base.Option.fold channel_dim ~init ~f:handle_cdim in
  Lazy.force_val result
2 Likes

This is called get_lazy in Containers. I would expect a version using Lazy to be slower.

2 Likes

It looks like you lazy-based solution is more verbose than a simple pattern matching:

  match channel_dim with
  | None -> linspace (t, x_max) |> Tensor.unsqueeze ~dim:1
  | Some dim ->
    let splits = Tensor.split t ~split_size:1 ~dim in
    let x_maxs = List.map splits ~f:(Fn.compose Tensor.to_float0_exn Tensor.maximum) in
    let ts = List.zip_exn splits x_maxs |> List.map ~f:linspace in
    Tensor.stack ts ~dim:1

I also find it lighter syntactically than creating two lambdas to pass to a utility function.

3 Likes

I agree that it is simpler in this case. I was trying to avoid a match statement since reading “fold” tells me that the Option container is removed but with a match statement I have to read the branches to figure out what happens to the container. My reasoning is similar to why one would prefer map over a match statement. Maybe it’s just a matter of personal taste. I will go with a match statement in this case but it would have been nicer if init accepted a thunk

Generally speaking, OCaml doesn’t have a call-by-name function parameter feature like Scala. In OCaml we need to be explicit about delayed evaluation. So usually we end up using a function or a lazy value.

The main exception to this is for the || (or) logical operator, which does need to have delayed evaluation for short-circuiting to work like one would expect. This is a special case.

2 Likes

This is … fertile food for thought. One says to oneself that lazy itself is a counter-example: its argument is lazily evaluated, right?

And so, perhaps what one might want, is a systematic methodology for recognizing arguments that are to be evaluated lazily. Something like (inventing syntax that nobody should take seriously):

[where “<:” is “we’re being lazy”]

let f <:x = .... bla bla ... eventually use x ...

and then you could use it with

f ~<:e

The type of f would be something like

val f : <:int -> string

[let’s assume that the x argument of f evaluated to an int and the body of f evaluates to a string]

So really, the type of f is something like int lazy_t -> string but with an explicit name there.

Why the explicit name? Not quite sure: I’m basically just riffing off of @yawaramin 's comment.

Anyway, it’s not a crazy idea, is what I’m saying. Not a crazy idea. But hey, I could be wrong/wrong/WRONG.

1 Like

That’s why I said ‘The main exception’, to avoid going into a rabbit hole :wink: (also, lazy is not a function i.e. value-level, it’s a language-level syntax. || is the only truly value-level operator with lazy evaluation)

1 Like

grin I’m only half-kidding, of course, Yawar. Half, b/c I can see the sense in “well, why not? It might be useful!” But also, “boy howdy, that’s a big, biiig can of worms, mang”.

I realize this thread has spun off on a tangent, but I think it should be noted that Lazy.force is not concurrency-safe. So making ways of hiding or implicitly using it before there is an effect system in place to expose “forcers” seems like a bad idea.

1 Like

There’s no point using lazy here since you’re immediately forcing, you can just use a normal thunk:

  let init () = linspace (t, x_max) |> Tensor.unsqueeze ~dim:1 in
  let handle_cdim _ dim () =
    let splits = Tensor.split t ~split_size:1 ~dim in
    let x_maxs = List.map splits ~f:(Fn.compose Tensor.to_float0_exn Tensor.maximum) in
    let ts = List.zip_exn splits x_maxs |> List.map ~f:linspace in
    Tensor.stack ts ~dim:1
  in
  let result = Base.Option.fold channel_dim ~init ~f:handle_cdim () in
  result
2 Likes

I went with this way of writing it since it avoided Lazy while also letting me use fold combinator. Thanks everyone for your inputs!

&& also does lazy evaluation. Is it not just as much of a value-level operator?

You’re right of course. It slipped my mind.