Monads in Scala Part Two: More Maybes
来源:互联网 发布:为什么网络ip地址冲突 编辑:程序博客网 时间:2024/06/04 00:46
scabl: Monads in Scala Part Two: More Maybes
http://scabl.blogspot.com/2013/03/monads-in-scala-2.html
Monads in Scala Part Two: More Maybes
In the first installment of this series, we translated the examples from the first page of thechapter 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.
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:
Now when mathematicians say things like "log square root", they mean you should take the square root first, and then take the log of that. Some people, like myself, might find this confusing, so I wanted to point that out. The initial implementation of safeLogSqrtin the example simply checks the outcome of sqrt before passing it on to log, like so:
This is a mouthful, especially when the unsafe version is as simple as this:
Which, according to the "left unit" Monad law, can be simplified to this:
I'm not sure why the Haskell example uses the longer form here. My guess is that it's because return x >>= safeSqrt >>= safeLog looks sexier than safeSqrt(x) >>= safeLog, but that's just my best guess.
Before moving on, here's a version of safeLogSqrt that uses a for loop:
Let's step back and examine what's going on here. In Scala, when we follow a function's name with an underscore, we are indicating that we are talking about the function itself, and not trying to apply that function. scala.math.log is a scala.Function1, and as such, has a compose method that takes a scala.Function1 as an argument. The result is a new function that behaves by calling sqrt first, and then calling log on the result of that.
At this point, the wikibook brings in the Kleisli composition operator, <=<, and shows how you can define a safeLogSqrt with similar brevity, like so:
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:
We are producing a function that takes an A as input and produces a Maybe[C]. We first apply g, which takes an A as input and produces a Maybe[B]. Then we flatMap theMaybe[B] with f, which takes a B as input and produces a Maybe[C]. Now we can define safeLogSqrt as follows:
This is nice, but wouldn't it be better if we could achieve this using an infix notation, to match scala.math.log _ compose scala.math.sqrt _? We cannot simply add a kleisliCompose method to Function1. What we do in Scala >= 2.10 in these circumstances is build an implicit class that contains the desired new method. Let's take a look at our implicit class, and then break it down:
The kleisliCompose method itself is the same, except that type parameters B and C, and parameter f, have been lifted out of the method and into the enclosing class. TheMaybeFunction constructor takes a single Function1 as argument - in particular, aFunction1 that returns a Maybe.
Let's take note that MaybeFunction is implicit, and that it has a methodkleisliCompose. 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 ofsafeLog _. 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 callkleisliCompose 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:
Now the test. Just a word of note here, I am switching my tests from just using asserts to using ScalaTest's ShouldMatchers. I'm using "org.scalatest" %% "scalatest" % "2.0.M5b", but any recent version should work. I have 5 versions ofsafeLogSqrt, so testing four pairs will do:
As you will recall, the left unit Monad law states that m >>= return = m. Let's assert this over all of our test data:
For right unit and associativity, we need to seed a list of all possible Maybes. We'll throwMaybeNot in with the rest of our test data wrapped in Justs:
We have a couple more Monad tests from last time. I won't go into the details, and you can check it out in the source code. I want to get down to business, which is calling this function over our two data sets:
Before writing any lookup methods, we need to seed the maps. First, we'll generate a hundred or so pieces of test data for each type:
We're going to generate the maps in such a way that there is at least one test value for every permutation of Just and MaybeNot. First, we agree that in every key/value pair in a map, the key and the value relate back to the same Int. Then, we filter each map down to the pairs whose underlying Int is divisible by some small prime. Using 2, 3, and 5 over a hundred element test set will give all permutations. Here's how it looks:
We need to add a conversion from Option to Maybe to translate the result of Map#get. A natural place for this is in the Maybe companion object:
We'll define the single-level lookup methods in terms of a generic lookup function that also takes the Map as parameter:
Finally, we can implement the lookup methods that span multiple dictionaries with forloops or flatMaps:
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 MaybeNotwhen 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:
12345
def safeSqrt(d: Double): Maybe[Double] =if (d >= 0) Just(scala.math.sqrt(d)) else MaybeNotdef safeLog(d: Double): Maybe[Double] =if (d > 0) Just(scala.math.log(d)) else MaybeNot
12345
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))
12
def safeLogSqrt1(d: Double): Maybe[Double] =Just(d) flatMap safeSqrt flatMap safeLog
12
def safeLogSqrt2(d: Double): Maybe[Double] =safeSqrt(d) flatMap safeLog
Before moving on, here's a version of safeLogSqrt that uses a for loop:
12
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
12
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:
1234567
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}
12
def safeLogSqrt4(d: Double): Maybe[Double] =kleisliCompose(safeLog _, safeSqrt _)(d)
123456
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 methodkleisliCompose. 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 ofsafeLog _. 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 callkleisliCompose 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:
12
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 aJust and a MaybeNot. I've also included a value (0) for which safeSqrt will return aJust, 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] toMaybeNot; and Double to MaybeNot to MaybeNot.12345678
/** A series of doubles useful for testing safe methods. */val doubles = List[Double](-2, // log, sqrt produce NaN-1, // log, sqrt produce NaN0, // log produces -Infinity, sqrt produces 00.5, // log produces negative1, // log produces 02) // keeping things positive
12345678910111213141516171819202122232425
package maybeimport org.scalatest.FlatSpecimport org.scalatest.matchers.ShouldMatchersclass 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 Persondata. 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 FlatSpecbehavior clause. The function opens like this:1234567
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
12345678
it should "obey left unit monadic law" in {testItems foreach { a =>{ Just(a) flatMap f} should equal {f(a)}}}
12345678910111213141516171819
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 }}}}
1234567891011
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:1234
case class Name(name: String)case class Number(number: String)case class Registration(registration: String)case class TaxOwed(taxOwed: Double)
12345678910
private val seedTestData: List[Int] = (1 to 100).toListprivate 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 intToNameval numbers: List[Number] = seedTestData map intToNumberval registrations: List[Registration] = seedTestData map intToRegistrationval taxesOwed: List[TaxOwed] = seedTestData map intToTaxOwed
123456789101112131415161718192021
private def seedMap[A, B](intFilter: (Int) => Boolean,intToA: (Int) => A,intToB: (Int) => B): Map[A, B] = {import language.postfixOpsseedTestData filter intFilter map {i => intToA(i) -> intToB(i)} toMap}// only those divisible by 2 are in the number mapprivate val numberMap: Map[Name, Number] =seedMap(_ % 2 == 0, intToName, intToNumber)// only those divisible by 3 are in the registration mapprivate val registrationMap: Map[Number, Registration] =seedMap(_ % 3 == 0, intToNumber, intToRegistration)// only those divisible by 5 are in the tax mapprivate val taxOwedMap: Map[Registration, TaxOwed] =seedMap(_ % 5 == 0, intToRegistration, intToTaxOwed)
123456
object Maybe {def apply[A](option: Option[A]): Maybe[A] = option match {case Some(a) => Just(a)case None => MaybeNot}}
1234567891011
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)
123456789101112131415161718
def lookupRegistration1(name: Name): Maybe[Registration] =for (number <- lookupNumber(name);registration <- lookupRegistration(number))yield registrationdef lookupRegistration2(name: Name): Maybe[Registration] =lookupNumber(name) flatMap lookupRegistrationdef lookupTaxOwed1(name: Name): Maybe[TaxOwed] =for (number <- lookupNumber(name);registration <- lookupRegistration(number);taxOwed <- lookupTaxOwed(registration))yield taxOweddef 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:1234567891011
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 _)
Conclusion
These were fun examples that are common use-cases for a Maybe class. We learned about kleisli composition, and solidified our understanding of the Maybe Monad. As a Java refugee, it took me a while to get used to Scala's Option class as an alternative tonulls. But once you get the hang of it, it's really handy. Methods like map, flatMap,orElse, and getOrElse, are so much more elegant than the equivalent operations with Java nulls. All that being said, I am looking forward to a non-Maybe example.- Monads in Scala Part Two: More Maybes
- Monads in Scala Part One: Maybe[Person]
- scabl: Monads in Scala Part Three: Lisst[A]
- Monads are Elephants Part 4
- Windows rootkits in 2005, part two
- Two Part: Level 90 Heuksal In SRO
- two or more data types in declaration of 错误
- error: two or more data types in declaration specifiers
- Error: two or more data types in declaration of `main'
- two+or+more+data+types+in+declaration+of+`main'
- error: two or more data types in declaration specifiers
- error: two or more data types in declaration of 'main'
- error: two or more data types in declaration specifiers
- two or more data types in declaration of
- two or more data types in declaration of `main'
- two or more data types in declaration specifiers
- Monads for the Curious Programmer, Part 1 (中英文对照版)
- Monads for the Curious Programmer, Part 2 (中英文对照版)
- response.sendRedirect("")和request.getRequestDispatcher("").forward(req,resp);
- Git详解之二:Git基础
- 九九乘法表的各种输出形式(c++)
- 黑马程序员-GUI编程总结
- pthread_join(pth, NULL);
- Monads in Scala Part Two: More Maybes
- 应用程序端口分类
- ubuntu添加开机启动项
- 叙利亚局势
- 行人检测(Pedestrian Detection)资源
- K&R C vs ANSI C (error C2143: syntax error : missing ';' before 'type' in Visual Studio 2008)
- C语言学习笔记(一)
- broadcast
- 线段树的应用