For example, partial functions are a language feature in which you can declare functions that may or may not be defined for a given input. A partial function (in scala) inherits from Function1 (one-argument function) and adds two interesting methods:
- isDefinedAt - Check to see if the function "is defined" for a given input
- orElse - Apply another partial function if this one is "undefined" for a given input
As you can see (by the orElse method), partial functions can make a chain-of-command pattern easy to implement using only language-level features. The Lift web framework puts this to great use with their URL rewriting API.
So how does one define a partial function? It's as easy as defining a function made purely of case statements. Here's a function that is only defined for a particular string:
scala> val myPartial : PartialFunction[String,String] = {
| case "mymatchedstring" => "my output string"
| }
myPartial: PartialFunction[String,String] =
Notice how I only define one case that does *not* cover all potential strings. Here's some examples of using "isDefinedAt" to check if a partial function can apply:
scala> myPartial.isDefinedAt("mymatchedstring")
res1: Boolean = true
scala> myPartial.isDefinedAt("someotherstring")
res2: Boolean = false
What happens if you attempt to call a partial function on a string for which it is not defined?
scala> myPartial("mymatchedstring2")
scala.MatchError: mymatchedstring2
at $anonfun$1.apply(:4)
at $anonfun$1.apply(:4)
at .( :6)
at .( )
at RequestResult$.( :3)
at RequestResult$.( )
at RequestResult$result()
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMe...
This is a "MatchError" runtime exception. I'd recommend using the isDefinedAt function and avoiding a runtime exception, but the choice is yours.
Let's try adding the string "mymatchedstring2" to this partial function using orElse:
scala> val partial2 : PartialFunction[String,String] = {
| case "mymatchedstring2" => "Delegation FTW"
| }
partial2: PartialFunction[String,String] =
scala> val doublePartial = myPartial orElse partial2
doublePartial: PartialFunction[String,String] =
scala> doublePartial.isDefinedAt("mymatchedstring")
res5: Boolean = true
scala> doublePartial.isDefinedAt("mymatchedstring2")
res6: Boolean = true
scala> doublePartial.isDefinedAt("mymatchedstring3")
res7: Boolean = false
scala> doublePartial("mymatchedstring")
res8: String = my output string
scala> doublePartial("mymatchedstring2")
res9: String = Delegation FTW
scala> doublePartial("mymatchedstring3")
scala.MatchError: mymatchedstring3
at $anonfun$1.apply(:4)
at $anonfun$1.apply(:4)
at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:38)
at .( :8)
at .( )
at RequestResult$.( :3)
at RequestResult$.( )
at RequestResult$result()
at sun.reflect.NativeMethodAccessorImpl.invoke0(Nativ...
Neato, now we can very easily create a chain-of-command pattern without defining any new classes (at least *I* don't define any new classes, the compiler may do whatever it needs to do).
Let's try to make use of PartialFunctions for the Database Migration API I posted about several blog postings back. The goal here is going to be creating a method of executing raw SQL queries during a migration. We're aiming for something close to the following syntax:
class Migration1 extends Migration {
import MigrationDSL._
def upgrade(implicit ctx : MigrationContext) {
execute_script {
case "MySQL" => """do mysql query;"""
case "Derby" => """Do derby query;"""
}
}
...
}
The signature for the above execute will look something like the following:
trait MigrationContext {
def executeScript(queries : PartialFunction[String,String]) : Unit
...
}
object MigrationDSL {
def execute_script(queries : PartialFunction[String,String])(implicit ctx : MigrationContext) = ctx.executeScript(queries)
...
}
Things are working pretty well so far. We can try to detect the case when a migration is not defined for a given database vendor, and throw some sort of exception which will make the migration fail. What if we'd like to give feedback to the user on whether or not they've left any supported databases "out" of their function?
The preferred way to do that is via sealed traits,shown below:
sealed trait DbVendor {
}
case class MySQL() extends DbVendor;
case class Derby() extends DbVendor;
case class Oracle() extends DbVendor;
trait MigrationContext {
def executeScript(queries : PartialFunction[DbVendor,String]) : Unit
...
}
object MigrationDSL {
def execute_script(queries : PartialFunction[DbVendor,String])(implicit ctx : MigrationContext) = ctx.executeScript(queries)
...
}
Now with the following code...
execute_script {
case MySQL() => """do mysql query;"""
case Derby() => """Do derby query;"""
}
... you don't see the desired error message. Why? Because we're taking a 'partial' function, which by definition could be undefined in certain locations. To fix this we need to instead take a plain old vanilla function like below:
trait MigrationContext {
def executeScript(queries : Function[DbVendor,String]) : Unit
...
}
object MigrationDSL {
def execute_script(queries : Function[DbVendor,String])(implicit ctx : MigrationContext) = ctx.executeScript(queries)
...
}
This informs the scala compiler that we expect the function to work for *all* inputs. If we give the compiler a little encouragement (i.e. sealed classes/traits), it can now help us avoid "missing" possibilities in our case statements.
Now with the following code...
execute_script {
case MySQL() => """do mysql query;"""
case Derby() => """Do derby query;"""
}
.. will generate this error...
[WARNING] /home/josh/projects/blog/scala-db-migrate/src/main/java/com/blogspot/suereth/dbmigrate/test/Migration1.scala:16: warning: match is not exhaustive!
[WARNING] missing combination Oracle
[WARNING]
[WARNING] execute_script({
[WARNING] ^
[WARNING] one warning found
Now our users can tell if they are handling every defined database vendor. If they wish to ignore the message (or define a "generic" query to try to use, they can simply match on "_" (or wildcard) like so:
execute_script {
case MySQL() => """do mysql query;"""
case Derby() => """Do derby query;"""
case _ => """Do generic query;"""
}
Now we're no longer explicitly using partial functions, but our API can still make use of pattern matching.
In a future post I plan to discuss how we can use extractors to do slick things with defining queries for specific versions of database vendors (or just all versions).
1 comment:
Cool stuff.
Post a Comment