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
Changes from 8 commits
e644f7f
9117ffe
aa2e285
f783065
97b5f3f
f4d8b4f
56e4dce
1e9fcc3
8adf287
e2ba020
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,55 @@ | ||
package fpinscala.errorhandling | ||
|
||
|
||
import scala.{Option => _, Either => _, Left => _, Right => _, _} // hide std library `Option` and `Either`, since we are writing our own in this chapter | ||
|
||
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] = | ||
map(f) match { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be simplified by not going through There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's usually easier to implement
Or There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very nice. Funny thing: this is exactly what Martin Odersky talked about in his lecture on monads in the Reactive Programming course. |
||
case Right(Right(b)) => Right(b) | ||
case Right(Left(e)) => Left(e) | ||
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(a)) => Left(e) | ||
case (Right(a), Left(e)) => Left(e) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe use |
||
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 } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess it's nitpicking, but I like the short form: There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
@@ -33,5 +64,5 @@ object Either { | |
def Try[A](a: => A): Either[Exception, A] = | ||
try Right(a) | ||
catch { case e: Exception => Left(e) } | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why you do need the map? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gpoirier I need to return something of type |
||
|
||
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] | ||
|
@@ -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") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice :-)