Skip to content
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

MPN-922: complete exercises for Functional Programming in Scala, Chapter 4 #3

Merged
merged 10 commits into from Apr 21, 2015
46 changes: 38 additions & 8 deletions exercises/src/main/scala/fpinscala/errorhandling/Either.scala
@@ -1,24 +1,54 @@
package fpinscala.errorhandling


import scala.{Option => _, Either => _, Left => _, Right => _, _} // hide std library `Option` and `Either`, since we are writing our own in this chapter

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice :-)


sealed trait Either[+E,+A] {
def map[B](f: A => B): Either[E, B] = sys.error("todo")
def map[B](f: A => B): Either[E, B] =
this match {
case Right(a) => Right(f(a))
case Left(e) => Left(e)
}

def flatMap[EE >: E, B](f: A => Either[EE, B]): Either[EE, B] =
this match {
case Right(a) => f(a)
case Left(e) => Left(e)
}

def flatMap[EE >: E, B](f: A => Either[EE, B]): Either[EE, B] = sys.error("todo")
def orElse[EE >: E, B >: A](b: => Either[EE, B]): Either[EE, B] =
this match {
case Right(_) => this
case Left(_) => b
}

def orElse[EE >: E, B >: A](b: => Either[EE, B]): Either[EE, B] = sys.error("todo")
def map2[EE >: E, B, C](b: Either[EE, B])(f: (A, B) => C): Either[EE, C] =
flatMap { aVal => b.map { bVal => f(aVal, bVal) } }

def map2[EE >: E, B, C](b: Either[EE, B])(f: (A, B) => C): Either[EE, C] = sys.error("todo")
/*
Using map2Either, we can combine all errors into a single value as
long as the 'E2' type can act as a container type for multiple error
values.
*/
def map2Either[E2 >: E, A2, A3](e: Either[E2, A2])(leftF: (E2, E2) => E2)(rightF: (A, A2) => A3): Either[E2, A3] =
(this, e) match {
case (Right(a1), Right(a2)) => Right(rightF(a1, a2))
case (Left(e), Right(_)) => Left(e)
case (Right(_), Left(e)) => Left(e)
case (Left(e1), Left(e2)) => Left(leftF(e1, e2))
}
}

case class Left[+E](get: E) extends Either[E,Nothing]
case class Right[+A](get: A) extends Either[Nothing,A]

object Either {
def traverse[E,A,B](es: List[A])(f: A => Either[E, B]): Either[E, List[B]] = sys.error("todo")
def traverse[E,A,B](as: List[A])(f: A => Either[E, B]): Either[E, List[B]] =
as.foldRight(Right(List.empty): Either[E, List[B]]) { (a, ebs) =>
f(a).map2(ebs) { (b, bs) => b :: bs }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's nitpicking, but I like the short form: f(a).map2(ebs)(_ :: _)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gpoirier I like to name the args if there are more than one, for easier readability.

}

def sequence[E,A](es: List[Either[E,A]]): Either[E,List[A]] = sys.error("todo")
def sequence[E,A](es: List[Either[E,A]]): Either[E,List[A]] =
traverse(es)(identity)

def mean(xs: IndexedSeq[Double]): Either[String, Double] =
if (xs.isEmpty)
Expand All @@ -33,5 +63,5 @@ object Either {
def Try[A](a: => A): Either[Exception, A] =
try Right(a)
catch { case e: Exception => Left(e) }
}

}
45 changes: 34 additions & 11 deletions exercises/src/main/scala/fpinscala/errorhandling/Option.scala
@@ -1,18 +1,27 @@
package fpinscala.errorhandling


import scala.{Option => _, Some => _, Either => _, _} // hide std library `Option`, `Some` and `Either`, since we are writing our own in this chapter

sealed trait Option[+A] {
def map[B](f: A => B): Option[B] = sys.error("todo")
def map[B](f: A => B): Option[B] =
this match {
case Some(a) => Some(f(a))
case None => None
}

def getOrElse[B>:A](default: => B): B = sys.error("todo")
def getOrElse[B>:A](default: => B): B =
this match {
case Some(a) => a
case None => default
}

def flatMap[B](f: A => Option[B]): Option[B] = sys.error("todo")
def flatMap[B](f: A => Option[B]): Option[B] = map(f).getOrElse(None)

def orElse[B>:A](ob: => Option[B]): Option[B] = sys.error("todo")
def orElse[B>:A](ob: => Option[B]): Option[B] =
map(Some(_)).getOrElse(ob)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why you do need the map?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gpoirier I need to return something of type Option[B], which has to always be actually a Some. Leaving out the map doesn't typecheck because this.getOrElse(ob) is a type conflict: A from the 'get' branch and Option[B] from the 'or else' branch.


def filter(f: A => Boolean): Option[A] = sys.error("todo")
def filter(f: A => Boolean): Option[A] =
flatMap { a => if (f(a)) Some(a) else None }
}
case class Some[+A](get: A) extends Option[A]
case object None extends Option[Nothing]
Expand All @@ -38,11 +47,25 @@ object Option {
def mean(xs: Seq[Double]): Option[Double] =
if (xs.isEmpty) None
else Some(xs.sum / xs.length)
def variance(xs: Seq[Double]): Option[Double] = sys.error("todo")

def map2[A,B,C](a: Option[A], b: Option[B])(f: (A, B) => C): Option[C] = sys.error("todo")
def variance(xs: Seq[Double]): Option[Double] =
mean(xs).flatMap { m =>
mean(xs.map(x => Math.pow(x - m, 2))) }

def sequence[A](a: List[Option[A]]): Option[List[A]] = sys.error("todo")
def map2[A,B,C](a: Option[A], b: Option[B])(f: (A, B) => C): Option[C] =
a.flatMap { aVal => b.map { bVal => f(aVal, bVal) } }

def sequence[A](a: List[Option[A]]): Option[List[A]] =
a.foldRight(Some(List.empty): Option[List[A]]) { (a, b) =>
map2(a, b) { (a, b) => a :: b }
}

def traverse[A, B](as: List[A])(f: A => Option[B]): Option[List[B]] =
as.foldRight(Some(List.empty): Option[List[B]]) { (a, obs) =>
map2(f(a), obs) { (b, bs) => b :: bs }
}

def sequence_using_traverse[A](as: List[Option[A]]): Option[List[A]] =
traverse(as)(identity)
}

def traverse[A, B](a: List[A])(f: A => Option[B]): Option[List[B]] = sys.error("todo")
}
122 changes: 122 additions & 0 deletions exercises/src/test/scala/errorhandling/EitherSpec.scala
@@ -0,0 +1,122 @@
import org.specs2.mutable.Specification
import org.specs2.ScalaCheck

import fpinscala.errorhandling._

class EitherSpec extends Specification with ScalaCheck {
val leftObj = Left(1)
val rightObj = Right(1)
val rightObj2 = Right(2)

def add1(i: Int) = i + 1
def rightAdd1(i: Int) = Right(add1(i))
def addXY(x: Int, y: Int) = x + y

"map" should {
"map a right value using the given function" in {
rightObj.map(add1) mustEqual rightObj2
}

"map a left value to itself" in {
leftObj.map(add1) mustEqual leftObj
}
}

"flatMap" should {
"obey left identity law" in {
prop { n: Int =>
Right(n).flatMap(rightAdd1) mustEqual rightAdd1(n)
}
}

"obey right identity law" in {
prop { n: Int =>
val rightN = Right(n)
rightN.flatMap(Right(_)) mustEqual rightN
}
}

"obey associativity law" in {
def f(i: Int) = Right(i + 1)
def g(i: Int) = Right(i + 1)

prop { n: Int =>
val r = Right(n)

r.flatMap(f).flatMap(g) mustEqual {
r.flatMap { x => f(x).flatMap(g) }
}
}
}
}

"orElse" should {
"return the object value if it is a right value" in {
rightObj.orElse(rightObj2) mustEqual rightObj
}

"return the default value if the object is a left value" in {
leftObj.orElse(rightObj2) mustEqual rightObj2
}
}

"map2" should {
"return mapped value if both values are right" in {
rightObj.map2(rightObj2)(addXY) mustEqual Right(3)
}

"return first left value if first value is left" in {
leftObj.map2(rightObj)(addXY) mustEqual leftObj
}

"return second left value if second value is left" in {
rightObj.map2(leftObj)(addXY) mustEqual leftObj
}

"return first left value if both values are left" in {
leftObj.map2(Left(2))(addXY) mustEqual leftObj
}
}

"sequence" should {
"return a right value of a list of values" in {
Either.sequence(List(rightObj, rightObj2)) mustEqual {
Right(List(1, 2))
}
}

"return a left value from a list of values" in {
Either.sequence(List(rightObj, rightObj2, leftObj)) mustEqual {
leftObj
}
}

"return the first left value from several" in {
Either.sequence(List(leftObj, Left(2))) mustEqual leftObj
}
}

"map2Either" should {
case class Person(name: Name, age: Age)
sealed class Name(val value: String)
sealed class Age(val value: Int)

def mkName(name: String): Either[String, Name] =
if (name == "" || name == null) Left("Empty name")
else Right(new Name(name))

def mkAge(age: Int): Either[String, Age] =
if (age < 0) Left("Out of range age")
else Right(new Age(age))

def mkPerson(name: String, age: Int): Either[String, Person] =
mkName(name).map2Either(mkAge(age)) {
(e1, e2) => e1 + "," + e2
} { (name, age) => Person(name, age) }

"combine left values if more than one" in {
mkPerson("", -1) mustEqual Left("Empty name,Out of range age")
}
}
}

129 changes: 129 additions & 0 deletions exercises/src/test/scala/errorhandling/OptionSpec.scala
@@ -0,0 +1,129 @@
import org.specs2.mutable.Specification

import fpinscala.errorhandling._

class OptionSpec extends Specification {
val someObj = Some(1)
val someOtherObj = Some(2)
def add1(i: Int) = i + 1
def addSome1(i: Int) = Some(add1(i))
def addXY(x: Int, y: Int) = x + y

"map" should {
"map a value using the provided function" in {
someObj.map(add1) mustEqual someOtherObj
}

"map an absent value to an absent value" in {
None.map(add1) mustEqual None
}
}

"getOrElse" should {
"get the value out of a Some object" in {
someObj.getOrElse(2) mustEqual 1
}

"get the default value if given a None object" in {
None.getOrElse(2) mustEqual 2
}
}

"flatMap" should {
"map and then flatten" in {
someObj.flatMap(addSome1) mustEqual someOtherObj
}

"map an absent value" in {
None.flatMap(addSome1) mustEqual None
}

"map to a function that returns an absent value" in {
someObj.flatMap(_ => None) mustEqual None
}
}

"orElse" should {
"return the optional if it contains a value" in {
someObj.orElse(someOtherObj) mustEqual someObj
}

"return the default if the optional is missing a value" in {
None.orElse(someOtherObj) mustEqual someOtherObj
}
}

"filter" should {
"preserve the optional value if it meets the predicate condition" in {
someObj.filter(_ == 1) mustEqual someObj
}

"drop the optional value if it fails the predicate condition" in {
someObj.filter(_ != 1) mustEqual None
}

"pass through an absent optional value" in {
None.filter(_ == 1) mustEqual None
}
}

"variance" should {
"not exist for an empty list" in {
Option.variance(Seq()) mustEqual None
}

"exist for a non-empty list" in {
Option.variance(Seq(1)) mustEqual Some(0.0)
}
}

"map2" should {
"return mapped value if both inputs are present" in {
Option.map2(someObj, someOtherObj)(addXY) mustEqual Some(3)
}

"return absent value if first input is absent" in {
Option.map2(None, someObj)(addXY) mustEqual None
}

"return absent value if second input is absent" in {
Option.map2(someObj, None)(addXY) mustEqual None
}
}

"sequence" should {
"return some empty list if given an empty list" in {
Option.sequence(List.empty) mustEqual Some(List.empty)
}

"return absent value if given list with any absent values" in {
Option.sequence(List(someObj, someOtherObj, None)) mustEqual None
}

"return some list of values if given list with no absent values" in {
Option.sequence(List(someObj, someOtherObj)) mustEqual {
Some(List(1, 2))
}
}
}

"sequence_using_traverse" should {
"return some empty list if given an empty list" in {
Option.sequence_using_traverse(List.empty) mustEqual {
Some(List.empty)
}
}

"return absent value if given list with any absent values" in {
Option.sequence_using_traverse(
List(someObj, someOtherObj, None)
) mustEqual None
}

"return some list of values if given list with no absent values" in {
Option.sequence_using_traverse(
List(someObj, someOtherObj)
) mustEqual Some(List(1, 2))
}
}
}