Home > Net >  Understanding of Tree fold implementaion
Understanding of Tree fold implementaion

Time:05-03

I am new to scala and I now the basic of it. Due to my internet research I found this blog post about inverse Btree's. I do understand most of it but this section:

def fold[A, B](t:Tree[A] , z:B)(f:(B,A,B) => B): B = t match {
  case EmptyTree => z
  case Node(x,l,r) => f ( fold( l , z )(f) , x , fold( r , z )(f) )
}

What does this (f:(B,A,B) => B) mean? And also why do I need to enclose it here f ( fold( l , z )(f) , x , fold( r , z )(f) ) and after the recursion call?

CodePudding user response:

In Scala, fold is generally provided as a shorthand for a "reduce to a single value" expression that would otherwise be accomplished via pattern matching.

For example, Option#fold is essentially

// in Option[A]
def fold[B](ifEmpty: => B)(f: A => B): B = this match {
  case None => ifEmpty
  case Some(a) => f(a)
}

and Either#fold is essentially

// in Either[A, B]
def fold[C](fa: A => C, fb: B => C): C = this match {
  case Left(a) => fa(a)
  case Right(b) => fb(b)
}

and List.foldLeft is essentially

def foldLeft[B](z: B)(op: (B, A) => B) = this match {
  case Nil => z
  case head :: tail => tail.foldLeft(op(z, head))(op) // recursion!
}

(disclaimer: I didn't take these from the Scala source, this is just from my own understanding of the methods)

The general goal of a fold is to reduce the collection (i.e. Option / Seq/ Either / Tree) to a single value based on the f function(s) that you pass to it. The f is an anonymous function which typically serves the role of a "transformation for when the value is available" or a "combine next input with previous/recursive result" operation.

In the case of the Tree class from the blog post, Tree is a recursive data structure, so naturally reducing it to a single value will involve some recursion. That's why you see fold( l , z )(f) and fold( r , z )(f) inside the fold implementation. These are computing the fold results (of type B) for the node's left and right sub-trees. Then the f is used to combine those results with the node's own value (of type A).

It may help to rearrange and rename things:

def fold[A, B](tree: Tree[A], ifEmpty: B)(combine: (B, A, B) => B): B = t match {
  case EmptyTree => ifEmpty
  case Node(value, left, right) =>
    // recurse into `left` and `right`
    val leftResult = fold(left, ifEmpty)(combine)
    val rightResult = fold(right, ifEmpty)(combine)
    combine(leftResult, value, rightResult)
}

In the blog post, the author defined def size as an example usage of fold:

def size[T] (tree: Tree[T]) =
  fold(tree, 0: Int){(l,x,r) => l   r   1}

This might make more sense as

def size[T](tree: Tree[T]): Int =
  fold(tree, 0){ (leftSize, x, rightSize) => leftSize   rightSize   1 }

The conclusion of the blog post uses B = Tree[A] in its fold call to accomplish the tree inversion. The combine (i.e. f) function takes the already-inverted left and right sub-trees, and constructs a new Node that puts them in the opposite positions. The fold implementation takes care of the recursion and just calls your f / combine function to piece things together.

CodePudding user response:

The term you're looking for is called currying, in honor of Haskell Curry.

In Scala, functions can have multiple argument lists. You can think of the declaration

def fold[A, B](t:Tree[A] , z:B)(f:(B,A,B) => B): B

as being very similar to

def fold[A, B](t:Tree[A] , z:B, f:(B,A,B) => B): B

It's really just a function of three arguments. But treating it as two separate argument lists confers us several benefits.

Partial Application

First, some functions are frequently partially applied in a certain way. For instance, consider the const function, which takes two arguments and returns its first.

def const[A, B](a: A, b: B) = a

As a function of two arguments, it's basically useless. But it can be useful for creating closures. For instance,

{ (b: Any) => const(0, b) }

is a function which always returns zero for any input. And since this is the most common way to use this function, we split it into two argument lists.

def const[A, B](a: A)(b: B) = a

and now we can write const(0) to mean "a constant function of one argument that returns 0 unconditionally". It's the same as what we wrote before but much less wordy.

Implicits

Implicit parameters (or using clauses, as they're called in Scala 3) must have their own argument list. So if a function, say, takes three values and adds them together using a Monoid operation, we would write

def add3[A](a: A, b: A, c: A)(implicit m: Monoid[A]) =
  m.append(a, m.append(b, c))

And then, when we call the function, we can just pass the three explicit arguments and leave the entire second argument list to implicit resolution.

(Of course, in this particular case, context bounds are a shorter way to express the same idea, but that's not always possible)

Function Arguments

This is the use case most directly applicable to your fold function. If we define fold as a function of three arguments in one list, we have to call it something like this.

fold(myTree, myValue, { (b1, a, b2) => ... })

Which is not necessarily the most intuitive notation. But if there's an argument list which consists of only one argument, and that one argument is a function, we can omit the parentheses. So with our two-argument form, we can write

fold(myTree, myValue) {
  (b1, a, b2) => ...
}

Notice how it's starting to look more like if(...) { ... } or while(...) { ... }. We can emulate control constructs with this notation, so it feels more natural in the presence of other Scala code.

We can still choose to call the function with two sets of parentheses, as you noted in your recursive call, but when the person who's using our API calls it, they don't have to if they just want to provide a lambda that looks like a control block.

  • Related