In the first installment of this series, we translated the examples from the first page of the chapter on monads in the Haskell wikibook from Haskell to Scala. In this blog post, we will continue on with the second page. This page continues with more Maybe examples, which will help us to generalize our monadic laws test for Maybes a bit. We'll also learn about kleisli composition, which composes functions suitable for passing to flatMap.
All the source code presented here is available here on GitHub. I'm compiling against Scala 2.10.1. Some of this code will not compile under Scala 2.9.x.
Safe Functions
The first section of the wiki page discusses safe functions - versions of mathematical functions that normally produce a Just wrapping the result, but that produce MaybeNot when the input is not within the range of that mathematical function. (As a reminder, I am using MaybeNot where the Haskell examples using Nothing, to avoid confusion with the Nothing found in the Scala library.) This will allow us to "floor out" when chaining mathematical operations - anything along the way that produces a MaybeNot will cause the MaybeNot to propagate right on down the chain.I'm not sure what Haskell's numeric types look like, but we do already get this kind of behavior when using Java doubles that underlie Scala Doubles. For example, sqrt(-1.0d) will produce double value NaN, which stands for "not a number". If we then take the log of that result - log(NaN) - we will still get NaN. Basically any math operation where one of the operands is NaN will produce a NaN. So safe math functions may not be as useful in Java or Scala, but it's a good example, so let's roll with it.
Here are my safe versions of sqrt and log:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def safeSqrt(d: Double): Maybe[Double] = | |
if (d >= 0) Just(scala.math.sqrt(d)) else MaybeNot | |
def safeLog(d: Double): Maybe[Double] = | |
if (d > 0) Just(scala.math.log(d)) else MaybeNot |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def safeLogSqrt0(d: Double): Maybe[Double] = | |
safeSqrt(d) match { | |
case Just(d) => safeLog(d) | |
case MaybeNot => MaybeNot | |
} |
- def unsafeLogSqrt(d: Double): Double = log(sqrt(d))
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def safeLogSqrt1(d: Double): Maybe[Double] = | |
Just(d) flatMap safeSqrt flatMap safeLog |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def safeLogSqrt2(d: Double): Maybe[Double] = | |
safeSqrt(d) flatMap safeLog |
Before moving on, here's a version of safeLogSqrt that uses a for loop:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def safeLogSqrt3(d: Double): Maybe[Double] = | |
for (s <- safeSqrt(d); l <- safeLog(s)) yield l |
Kleisli Composition
The Haskell wikibook points out that you can produce an unsafe logSqrt function by simply composing the unsafe functions log and sqrt. For function composition, mathematicians use a little dot like this: ∘. In Haskell, the composition is done like this:- unsafeLogSqrt = log . sqrt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
val unsafeLogSqrt: (Double) => Double = | |
scala.math.log _ compose scala.math.sqrt _ |
At this point, the wikibook brings in the Kleisli composition operator, <=<, and shows how you can define a safeLogSqrt with similar brevity, like so:
- safeLogSqrt = safeLog <=< safeSqrt
As a first attempt at a kleisli composition operator in Scala, let's write a method that takes in two functions as arguments, and produces the kleisli composition of those two functions:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def kleisliCompose[A, B, C]( | |
f: (B) => Maybe[C], | |
g: (A) => Maybe[B]): | |
(A) => Maybe[C] = { | |
a: A => | |
for (b <- g(a); c <- f(b)) yield c | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def safeLogSqrt4(d: Double): Maybe[Double] = | |
kleisliCompose(safeLog _, safeSqrt _)(d) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
implicit class MaybeFunction[B, C](f: (B) => Maybe[C]) { | |
def kleisliCompose[A](g: (A) => Maybe[B]): (A) => Maybe[C] = { | |
a: A => | |
for (b <- g(a); c <- f(b)) yield c | |
} | |
} |
Let's take note that MaybeFunction is implicit, and that it has a method kleisliCompose. Now, when the compiler encounters something like safeLog _ kleisliCompose safeSqrt _, and tries to resolve the method call, it does not immediately find a method kleisliCompose in Function1, which is the type of safeLog _. Before it gives up, it looks for any implicit conversions it could use to transform safeLog _ into that has a method named kleisliCompose with an applicable signature. Assuming our MaybeFunction class is in the right scope, it will implicitly construct a MaybeFunction from the Function1, and call kleisliCompose on that.
This Pimp my Library pattern is available in languages like Ruby and Groovy via meta-programming. In Scala, it's done at compile-time in a type-safe way. While implicit classes are newly available in Scala 2.10, the same effect has been achieved for years with implicit functions. The behavior of implicit classes is more controlled than that of implicit functions, as the target of the type conversion is constrained to be the new, implicit class.
So let's go ahead and use this pimped Function1 to define our safeLogSqrt function:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def safeLogSqrt5(d: Double): Maybe[Double] = | |
(safeLog _ kleisliCompose safeSqrt _)(d) |
Testing Safe Functions
What about testing all the safe functions we have defined above? First of all, we could write tests that assure that all of the versions of safeLogSqrt presented above produce the same results. To do this, we first need to define a set of test data to use. To be sure to test every outcome, I've included input values for which safeLogSqrt will return both a Just and a MaybeNot. I've also included a value (0) for which safeSqrt will return a Just, but safeLogSqrt will return a MaybeNot. So I'm covering the three major code paths: Double to Just[Double] to Just[Double]; Double to Just[Double] to MaybeNot; and Double to MaybeNot to MaybeNot.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** A series of doubles useful for testing safe methods. */ | |
val doubles = List[Double]( | |
-2, // log, sqrt produce NaN | |
-1, // log, sqrt produce NaN | |
0, // log produces -Infinity, sqrt produces 0 | |
0.5, // log produces negative | |
1, // log produces 0 | |
2) // keeping things positive |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package maybe | |
import org.scalatest.FlatSpec | |
import org.scalatest.matchers.ShouldMatchers | |
class SafeMathSpec extends FlatSpec with ShouldMatchers { | |
import safe._ | |
behavior of "various implementations of safe.safeLogSqrt" | |
they should "agree over a range of test input" in { | |
shouldAgreeOverTestInputRange(safeLogSqrt1 _, safeLogSqrt2 _) | |
shouldAgreeOverTestInputRange(safeLogSqrt1 _, safeLogSqrt3 _) | |
shouldAgreeOverTestInputRange(safeLogSqrt1 _, safeLogSqrt4 _) | |
shouldAgreeOverTestInputRange(safeLogSqrt1 _, safeLogSqrt5 _) | |
} | |
def shouldAgreeOverTestInputRange( | |
safeLogSqrt1: (Double) => Maybe[Double], | |
safeLogSqrt2: (Double) => Maybe[Double]) { | |
doubles foreach { d => | |
(safeLogSqrt1(d)) should equal (safeLogSqrt2(d)) | |
} | |
} | |
} |
Testing Maybe Obeys Monad Laws
In the last blog post, we developed tests to assure that Maybe obeyed the monadic laws in regards to some sample Person data. We now have another data set that we can test the Maybe monad against. But the old version of the test is hard-coded to use Person data. Let's generalize that into a function that takes the required test data as arguments. Then we can call this function for different sets of test data. We can easily extrapolate over the types involved. We also pass a String description to include in a FlatSpec behavior clause. The function opens like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def maybeShouldObeyMonadLaws[A, B, C]( | |
testDataDescription: String, | |
testItems: List[A], | |
f: Function1[A, Maybe[B]], | |
g: Function1[B, Maybe[C]]) { | |
behavior of "Maybe monad with respect to " + testDataDescription | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
it should "obey left unit monadic law" in { | |
testItems foreach { a => | |
{ Just(a) flatMap f | |
} should equal { | |
f(a) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
val maybes = MaybeNot +: (testItems map { Just(_) }) | |
it should "obey right unit monadic law" in { | |
maybes foreach { m => | |
{ m flatMap { Just(_) } | |
} should equal { | |
m | |
} | |
} | |
} | |
it should "obey associativity monadic law" in { | |
maybes foreach { m => | |
{ m flatMap f flatMap g | |
} should equal { | |
m flatMap { a => f(a) flatMap g } | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
maybeShouldObeyMonadLaws( | |
"Person data", | |
Person.persons, | |
{ p: Person => p.mother }, | |
{ p: Person => p.father }) | |
maybeShouldObeyMonadLaws( | |
"safe math operations", | |
safe.doubles, | |
safe.safeSqrt _, | |
safe.safeLog _) |
Lookup Tables
The next major section of the wikibook page is on lookup tables. A couple of things perplexed me here. First, they are using a list of pairs as a lookup table. I did check, and it seems Haskell has a perfectly serviceable Map class. Using a list of pairs instead of a library collection class would never occur to me. I'm probably spoiled by the quality of Scala's collection library. The second thing that seemed quite different to me was the fact that they are putting raw strings and numbers into the map. There are two maps in their example that are semantically different, but both have the same type signature: string to string. Haskell has such a rich type system, it seems like they could easily do better than this. They probably made these choices to keep the example simple, and to focus the discussion on the use of Monads. But it wouldn't be natural Scala without using case classes and maps. Typing the data is trivial in Scala:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
case class Name(name: String) | |
case class Number(number: String) | |
case class Registration(registration: String) | |
case class TaxOwed(taxOwed: Double) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private val seedTestData: List[Int] = (1 to 100).toList | |
private def intToName(i: Int): Name = Name(i.toString) | |
private def intToNumber(i: Int): Number = Number(i.toString) | |
private def intToRegistration(i: Int): Registration = Registration(i.toString) | |
private def intToTaxOwed(i: Int): TaxOwed = TaxOwed(i.toDouble) | |
val names: List[Name] = seedTestData map intToName | |
val numbers: List[Number] = seedTestData map intToNumber | |
val registrations: List[Registration] = seedTestData map intToRegistration | |
val taxesOwed: List[TaxOwed] = seedTestData map intToTaxOwed |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private def seedMap[A, B]( | |
intFilter: (Int) => Boolean, | |
intToA: (Int) => A, | |
intToB: (Int) => B): Map[A, B] = { | |
import language.postfixOps | |
seedTestData filter intFilter map { | |
i => intToA(i) -> intToB(i) | |
} toMap | |
} | |
// only those divisible by 2 are in the number map | |
private val numberMap: Map[Name, Number] = | |
seedMap(_ % 2 == 0, intToName, intToNumber) | |
// only those divisible by 3 are in the registration map | |
private val registrationMap: Map[Number, Registration] = | |
seedMap(_ % 3 == 0, intToNumber, intToRegistration) | |
// only those divisible by 5 are in the tax map | |
private val taxOwedMap: Map[Registration, TaxOwed] = | |
seedMap(_ % 5 == 0, intToRegistration, intToTaxOwed) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
object Maybe { | |
def apply[A](option: Option[A]): Maybe[A] = option match { | |
case Some(a) => Just(a) | |
case None => MaybeNot | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private def lookup[A, B](map: Map[A, B])(a: A): Maybe[B] = | |
Maybe(map.get(a)) | |
def lookupNumber(name: Name): Maybe[Number] = | |
lookup(numberMap)(name) | |
def lookupRegistration(number: Number): Maybe[Registration] = | |
lookup(registrationMap)(number) | |
def lookupTaxOwed(registration: Registration): Maybe[TaxOwed] = | |
lookup(taxOwedMap)(registration) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def lookupRegistration1(name: Name): Maybe[Registration] = | |
for ( | |
number <- lookupNumber(name); | |
registration <- lookupRegistration(number)) | |
yield registration | |
def lookupRegistration2(name: Name): Maybe[Registration] = | |
lookupNumber(name) flatMap lookupRegistration | |
def lookupTaxOwed1(name: Name): Maybe[TaxOwed] = | |
for ( | |
number <- lookupNumber(name); | |
registration <- lookupRegistration(number); | |
taxOwed <- lookupTaxOwed(registration)) | |
yield taxOwed | |
def lookupTaxOwed2(name: Name): Maybe[TaxOwed] = | |
lookupNumber(name) flatMap lookupRegistration flatMap lookupTaxOwed |
More Testing Maybe Obeys Monad Laws
We can easily add two more Monad laws tests, like so:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
maybeShouldObeyMonadLaws( | |
"looking up Numbers and Registrations by Name", | |
lookup.names, | |
lookup.lookupNumber _, | |
lookup.lookupRegistration _) | |
maybeShouldObeyMonadLaws( | |
"looking up Registrations and TaxesOwed by Number", | |
lookup.numbers, | |
lookup.lookupRegistration _, | |
lookup.lookupTaxOwed _) |
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.