def map2[T, U](to1: Option[T], to2: => Option[T])(f: (T, T) => U): Option[U] = for {t1 <- to1; t2 <- to2} yield f(t1, t2)
Now, we decide that we'd like to log the result of this method (or maybe we just want to print it to the console). To do this the "traditional" way, we would need to write something like this:
def map2[T, U](to1: Option[T], to2: => Option[T])(f: (T, T) => U): Option[U] = { val logger = LoggerFactory.getLogger(getClass) val r = for {t1 <- to1; t2 <- to2} yield f(t1, t2) logger.debug(s"map2: ${r.toString}") r }
We have interrupted the flow of the expression, we've had to create a new variable (r), we've had to wrap it all in braces. Not cool!
So, I looked around for something that could do this the functional way. Unfortunately, I couldn't find anything. There is something in Scalaz but it looked complicated and maybe a little too general. So I decided (as I often do) to write my own and call it Spy.
One of the design decisions that I had to make was this: I don't want the Spy to have to be instantiated for every user invocation, or even every class that an invocation appears in. Yet I want each invocation/class to be able to customize the spying behavior somewhat. That's where implicits come (again) to the rescue. But the spy-function (the one that actually does something with a String formed from the expression's value) needs to be found. The natural type of the spy-function is String=>Unit but it turns out that the implicits mechanism couldn't deal with something so ordinary so I changed it to String=>Spy where the Spy class is essentially just a wrapper that has no real significance. Then, the implicit value for the spy-function (if any) could be found and used.
One example of a need to customize the implicit value is to forget about logging and simply write to the console. I tried to make this as easy as possible. See the specification in the repo (linked at the bottom) for an example of this.
Here, using the default slf4j logging mechanism, is the map2 function with logging. Note that, when you use the default mechanism, you must provide an implicit value of a logger in scope. A convenience method has been provided for this as shown below.
implicit val logger = Spy.getLogger(getClass) def map2[T, U](to1: Option[T], to2: => Option[T])(f: (T, T) => U): Option[U] = Spy.spy(s"map2($to1,$to2)",for {t1 <- to1; t2 <- to2} yield f(t1, t2))
We test it using the following specification for map2 (unchanged):
"map2(Option)" should "succeed" in { val one = Some(1) val two = Some(2) def sum(x: Int, y: Int) = x + y map2(one, two)(sum) should matchPattern { case Some(3) => } map2(one, None)(sum) should matchPattern { case None => } }
And the resulting entries in the log file are:
2016-12-06 22:35:04,123 DEBUG com.phasmid.laScala.fp.FP$ - spy: map2(Some(1),Some(2)): Some(3)
2016-12-06 22:35:04,128 DEBUG com.phasmid.laScala.fp.FP$ - spy: map2(Some(1),None): None
If you're interested in using this you can find the Spy class, together with its SpySpec, in my LaScala project.
This comment has been removed by a blog administrator.
ReplyDelete