As part of an exercise from FP in Scala, I'm working on the implementation of foldLeft on an IndexedSeq.
I wrote 2 functions:
def foldLeft[A, B](as: IndexedSeq[A])(z: B)(f: (B, A) => B): B = {
def go(bs: IndexedSeq[A], acc: B): B = {
if (bs.isEmpty) acc
else go(bs.tail, f(acc, bs.head))
}
go(as, z)
}
And, then the pattern match way:
def foldLeftPM[A, B](as: IndexedSeq[A])(z: B)(f: (B, A) => B): B = {
def go(bs: IndexedSeq[A], acc: B): B = bs match {
case x +: xs => go(xs, f(acc, x))
case _ => acc
}
go(as, z)
}
EDIT Note that I got the +: operator from dhgs's answer. It appears to be a member of IndexedSeq's class or its parent since it's available without defining per the linked post.
Is either way better (from a performance or idiomatic Scala point of view)?
The pattern match is definitely more idiomatic.
For performance, they should be about the same, since they are exactly equivalent.
Though only benchmarking would decide, and that includes a lot of assumptions.
Related
In functional programmming, there are two important methods named foldLeft and foldRight. Here is the implementation of foldRight
sealed trait List[+A]
case object Nil extends List[Nothing]
case class Cons[+A](head: A, tails: List[A]) extends List[A]
def foldRight[A, B](ls: List[A], z: B)(f: (A, B) => B): B = ls match {
case Nil => z
case Cons(x, xs) => f(x, foldRight(xs, z)(f))
}
And here is the implementation of foldLeft:
#annotation.tailrec
def foldLeft[A, B](ls: List[A], z: B)(f: (B, A) => B): B = ls match {
case Nil => z
case Cons(x, xs) => foldLeft(xs, f(z, x))(f)
}
}
My question is: I read from many documents, they often put order of f function is: f: (B, A) => B instead of f: (A, B) => B. Why this definition is better? Because if we use otherwise way, it will have same signature with foldLeft, and it will be better.
Because foldLeft "turns around" the cons structure in its traversal:
foldRight(Cons(1, Cons(2, Nil)), z)(f) ~> f(1, f(2, z))
^ ^
A B
foldLeft(Cons(1, Cons(2, Nil)), z)(f) ~> f(f(z, 2), 1)
^ ^
B A
And since it visits the conses in opposite order, the types are also traditionally flipped. Of course the arguments could be swapped, but if you have a non-commutative operation, and expect the "traditional" behaviour, you'll be surprised.
...if we use otherwise way, it will have same signature with foldLeft, and it will be better.
No, I think that would not be better. If they had the same signature then the compiler would not be able to catch it if you intend to use one but accidentally type in the other.
The A/B order is also a handy reminder of where B (initial or "zero") value goes in relationship to the collection of A elements.
foldLeft: B-->>A, A, A, ... // (f: (B, A) => B)
foldRight: ... A, A, A<<--B // (f: (A, B) => B)
Functional Programming in Scala defines Applicative#traverse as:
def traverse[A,B](as: List[A])(f: A => F[B]): F[List[B]]
as.foldRight(unit(List[B]()))((a, fbs) => map2(f(a), fbs)(_ :: _))
However, I implemented this function as:
def traverse[A,B](as: List[A])(f: A => F[B]): F[List[B]] =
as.foldRight(unit(List[B]()))((elem, acc) => map2(acc, f(elem))(_ :+ _))
With unit and map2 defined as:
def map2[A,B,C](fa: F[A], fb: F[B])(f: (A,B) => C): F[C]
def unit[A](a: => A): F[A]
As I understand my implementation, the map2(acc, f(elem))(_ :+ _)) will behave as so:
for each (element, accumulator), call map2(acc, f(elem)(_ :+ _))
to append the result of f(elem) (type F[B]) to the accumulator (type F[List[B]])
For my implementation, then, in the (f: (A,B) => C) part of traverse's map2 calls, List[B] appends B to itself.
map2 accepts arguments F[A] and F[B], but the function argument f operates on the A and B types. In the FP in Scala solution, B gets added to List[B] via the :: operator.
Is that right?
You are correct in your understanding of what is going on. Note, however, that your implementation will produce a list in the reverse order of what is expected, so if you used the identity monad as F, you'd get back a reversed list, instead of the same list.
Continuing to work on Functional Programming in Scala exercises, I'm working on implementing foldRight on an IndexedSeq type.
Since foldRight will evaluate with right associativity, I created the following operator for pattern matching.
object :++ {
def unapply[T](s: Seq[T]) = s.lastOption.map(last =>
(last, s.take(s.length - 1)))
}
And then implemented as so:
object IndexedSeqFoldable extends Foldable[IndexedSeq] {
override def foldRight[A, B](as: IndexedSeq[A])(z: B)(f: (A, B) => B): B = {
def go(bs: Seq[A], acc: B): B = bs match {
case x :++ xs => go(xs, f(x, acc))
case _ => acc
}
go(as, z)
}
Ignoring the fact that foldRight can be written with foldLeft, how does my approach hold up?
I feel insertSortRight is less efficient than insertSortLeft because insertSortRight needs to call List.last (which is O(n)) as one of the arguments to insert(), where insertSortLeft calls List.head (which is O(1)) as one of the arguments to insert().
Is this understanding correct? Thanks.
def insertSortRight(unsorted: List[Int]) : List[Int] = {
(unsorted :\ List[Int]()) ((a, b) => insert(a, b))
}
def insertSortLeft(unsorted: List[Int]) : List[Int] = {
(List[Int]() /: unsorted) ((a, b) => insert(b, a))
}
def insert(a: Int, list: List[Int]) : List[Int] = list match {
case List() => List(a)
case y::ys => if (a > y) y::insert(a, ys) else a::y::ys
}
DHG answered "always prefer left folding". But, Programming in Scala has an example the other way.
def flattenLeft[T](xss: List[List[T]]) = (List[T]() /: xss) (_ ::: )
def flattenRight[T](xss: List[List[T]]) = (xss :~List[T]()) ( ::: _)
I guess that is because flattenRight in this case is achieved by just one function call, while flattenLeft is achieved by n function call?
So, for a List, since head operations are desired, foldLeft is the natural choice. That way you work through the list from left to right, always taking the head. As you can see, its implementation (on LinearSeqOptimized) simply uses a while-loop and traverses once.
override /*TraversableLike*/
def foldLeft[B](z: B)(f: (B, A) => B): B = {
var acc = z
var these = this
while (!these.isEmpty) {
acc = f(acc, these.head)
these = these.tail
}
acc
}
It seems like 'foldRight' would be O(n^2) since, in order to take the last element, you have to traverse the n elements of the List n times, but the library actually optimizes this for you. Behind the scenes, foldRight is implemented like this (also on LinearSeqOptimized):
def foldRight[B](z: B)(f: (A, B) => B): B =
if (this.isEmpty) z
else f(head, tail.foldRight(z)(f))
As you can see, this function is constructed by recursively calling foldRight on the tail, holding each head on the stack, and applying the function to each head in reverse order after reaching the last element.
I found a good article, about call with current continuation patterns. As I understand, they use Scheme and undelimited continuations. Can the patterns from the article be implemented in Scala? Is there any article about delimited continuations patterns in Scala ?
Yes, they absolutely can. callCC looks like this in Scala:
def callCC[R, A, B](f: (A => Cont[R, B]) => Cont[R, A]): Cont[R, A] =
Cont(k => f(a => Cont(_ => k(a))) run k)
Where Cont is a data structure that captures a continuation:
case class Cont[R, A](run: (A => R) => R) {
def flatMap[B](f: A => Cont[R, B]): Cont[R, B] =
Cont(k => run(a => f(a) run k))
def map[B](f: A => B): Cont[R, B] =
Cont(k => run(a => k(f(a))))
}
Here's how you might use it to simulate checked exceptions:
def divExcpt[R](x: Int, y: Int, h: String => Cont[R, Int]): Cont[R, Int] =
callCC[R, Int, String](ok => for {
err <- callCC[R, String, Unit](notOK => for {
_ <- if (y == 0) notOK("Denominator 0") else Cont[R, Unit](_(()))
r <- ok(x / y)
} yield r)
r <- h(err)
} yield r)
You would call this function as follows:
scala> divExcpt(10, 2, error) run println
5
scala> divExcpt(10, 0, error) run println
java.lang.RuntimeException: Denominator 0
Scala has an implementation of typed delimited continuations which used to be shipped with the compiler and standard library, but has been extracted to an external module and pretty much left to rot since then. It's a great shame, and I encourage anyone who's interested in delimited continuations to show that they care about its existence by using and contributing to it.