5

I'm sure there's a good reason for this, but I'm not seeing it.

Fold on (say) List returns

the result of applying fold operator op between all the elements and z

It has an obvious relationship with foldLeft and foldRight that do the same thing but with defined order (and so don't need associative operators)

Fold on Option returns

Returns the result of applying f to this scala.Option's value if the scala.Option is nonempty. Otherwise, evaluates expression ifEmpty.

ifEmpty is (in the position of) the z for a List. f is (in the position of) the op

For None (which, using my mental model of Option as a "container" that may or may not contain a value, is an "empty" container), things are OK, Option.fold returns the zero (value of ifEmpty).

For Some(x), though, shouldn't f take two params, z and x so it's consistent with the fold on sequences (including having a similar relationship to foldLeft and foldRight).

There's definitely a utility argument against this - having f just take x as a parameter in practice probably more convenient. In most cases, if it did also take z that would be ignored. But consistency matters too...

So can someone explain to me why fold on Option is still a "proper" fold?

The Archetypal Paul
  • 41,321
  • 20
  • 104
  • 134
  • But `Some#fold` does take 2 arguments. https://github.com/scala/scala/blob/v2.11.3/src/library/scala/Option.scala#L1 I am sorry I dont understand the question – Jatin Oct 17 '14 at 11:25
  • It does indeed take two arguments; but the arguments are just in separate argument lists. – Jesper Oct 17 '14 at 11:26
  • I'm asking why the second argument is not a binary operator. Not why fold itself takes two arguments - as you say, it does! – The Archetypal Paul Oct 17 '14 at 11:27
  • @Jesper, yes, but that's not my question. I've re-titled it to make it clearer (I hope) – The Archetypal Paul Oct 17 '14 at 11:28
  • 5
    I wrote [a blog post](http://meta.plasm.us/posts/2014/07/04/foldable-considered-confusing/) about a similar question a few months ago. – Travis Brown Oct 17 '14 at 11:34
  • @TravisBrown, thanks, interesting. I'm not sure I get the point about the two args corresponding to the constructors, though. Why is that more the the right way to think about a fold? Pointers to other explanations welcomed :) – The Archetypal Paul Oct 17 '14 at 11:49
  • I feel you answered your own question. On a list, you get the default value in the Nil-case, and need a 2-argument function where one argument is the accumulator. For a single-value container like Some, there is nothing to accumulate. It doesn't seem sensible to define a function that always essentially ignores one parameter just for consistency. – Justin Kaeser Oct 17 '14 at 11:56
  • @JustinKaeser, that would be more powerful an argument (for me) if foldLeft and foldRight took the same approach. But there, the value of consistency seems to have been more important that the 'nothing to accumulate' argument – The Archetypal Paul Oct 17 '14 at 11:58
  • The difference is, foldLeft/foldRight are provided only by implicit conversion to Iterable. `fold` is in this case defined directly on Option. I presume the method is overridden only for fold because in the other cases it doesn't really make sense. – Justin Kaeser Oct 17 '14 at 12:13
  • I don't think I agree. It makes sense to treat all folds (`fold`, `foldLeft`, `foldRight`) the same. If it's for utility (not passing an arg that's ignored), that argument is just as strong for `foldLeft`. It doesn't _have_ to be implemented by implicit conversion to `Iterable` (or conversely, `fold` could have been implemented that way too). Your argument seems to come down to "that's the way it is", where I was asking why. – The Archetypal Paul Oct 17 '14 at 12:19
  • I can't tell you the rationale the library designers used, but it does make sense to me that if you are redefining a fold for Option, it is only really reasonable to redefine one of them, as the others would have exactly the same type and result. – Justin Kaeser Oct 17 '14 at 13:03
  • 5
    The structure of a `fold` function is always going to be related to shape of the container. On a tree, you'd have to have 3 arguments: the accumlator, the value of the node itself, and a `Seq` of values of the children. The binary fold we know and love shouldn't be considered universal. I wrote an answer on this a while back: http://stackoverflow.com/questions/24132325/play-framework-form-fold-method-naming-rationale/24133348#24133348. – acjay Oct 17 '14 at 13:56
  • `scala.concurrent.Future` is also another example where the `fold` takes three parameters: `def fold[T, R](futures: Seq[Future[T]])(zero: R)(op: (R, T) => R)`. So as mentioned by @acjay it is always dependent on the shape of the type `fold` is defined in. – Nader Ghanbari Oct 20 '14 at 15:13

2 Answers2

2

Why should it be?

The reason fold "normally" takes a binary operator is because, "normally" is used over a list that can be processed one by one with binary operators (and the seed value z).

Fold on an option is, de facto, a map function with a default case. If you look at its definition, it literally is:

if (isEmpty) ifEmpty else f(this.get)

Since you only have one possible argument, f has to be an unary operator. One could argue to have an option fold that takes a binary operator and used the ifEmpty value as the seed in case the option is defined, but your sensible seed value and your sensible value for when the option is empty may be greatly different.

As someone pointed out, for different structures you need different arities (such as for trees), because the "sensible" application of a reduction has different structures.

Diego Martinoia
  • 4,592
  • 1
  • 17
  • 36
1

scala.Option.fold is a mistake; poor API design.

Option {
  // This is equivalent to:  map f getOrElse ifEmpty.
  def fold[B](ifEmpty: => B)(f: A => B): B = if (isEmpty) ifEmpty else f(this.get)
}

The Option.fold method may be handy, but it's no fold (likewise for Either.fold).

TraversableOnce {
  // List, et alia
  def fold[A1 >: A](z: A1)(op: (A1, A1) => A1): A1 = foldLeft(z)(op)
}

The benefit of a standard method is that the concept and type signature are consistent (no need to review the docs again).

Also, Option.foldLeft is consistent, and does take a binary operator.

601
  • 53
  • 6