In my previous post I presented a simple
Publisher
that I used to
decouple my Swing actors from their targets.
Reader nairb774
pointed out that the standard Scala library includes
a Publisher
class.
In fact, there are two Publisher
classes in Scala,
scala.collection.mutable.Publisher
and
scala.swing.Publisher
.
Although I like my publisher class better,
the swing publisher did have one feature that I thought was useful:
it accepted as a callback a PartialFunction
rather than, as mine
did, a Function1
.
That would mean, I thought, that I could pass in a case
statement as a callback.
For example, continuing the Mimprint example from my previous post, if I were only interested in
Enabled
events published by a particular
publisher, rather than explicitly checking this in my callback with
an isInstanceOf
or a match
statement
that includes a case _ =>
clause,
I could just use a one-line case statement:
My calling code inshowSingleViewerPublisher.subscribe { case e:Enabled => doSomething() }
Publisher
would call apply
on the PartialFunction
callback only
if a call to its isDefinedAt
method returned true, thus avoiding the
MatchError
that would occur
if I treated it like a Function1
and called its apply
method when the value was not Enabled
.
This seemed like useful functionality, so I decided to add it.
I thought it would be easy, but unfortunately it was not.
Consider the following three definitions that assign a case statement to a partial function, full function, or no explicit function type, respectively:
For the first line, the variableval pfv:PartialFunction[String,Unit] = { case "x" => println("Got x") } val ffv:Function1[String,Unit] = { case "x" => println("Got x") } val nfv = { case "x" => println("Got x") }
pfv
gets assigned a value which is a
PartialFunction
representing the case statement.
For the second line,
you might think that, since PartialFunction
extends Function1
and we
are assigning the same value to ffv
as we did to pfv
,
that the variable ffv
would be assigned a value which is a PartialFunction
,
just as for the value pfv
.
This is not the case.
The Scala Language Specification (SLS) explicitly states, in section 8.5, that the type of an anonymous function comprised of one or more case statements must be specified as either a
FunctionK
or a PartialFunction
,
and that the value generated by the compiler is different depending
on that specified target type.
So the value that gets assigned to ffv
is a Function1
, and
ffv.isInstanceOf[PartialFunction[_,_]]
evaluates to false.
Note that we could assign the value pfv
to the variable
ffv
, in which case ffv
would have a value
which is a PartialFunction
and
ffv.isInstanceOf[PartialFunction[_,_]]
would evaluate to true.
What happens if you don't specify the type, as in the third line above where we assign the same value to
nfv
?
You might think the compiler could infer the type of the resulting
value, but since, as specified in the SLS, the type must be explicitly
specified as either a FunctionK
or a PartialFunction
,
our assignment to nfv
is actually not a valid statement,
and it fails to compile.
It would be nice if the error message said something like
"You must explicitly specify either a FunctionK or a
PartialFunction for a case statement", but instead it gives
this relatively unhelpful message:
In my case, the situation in which I encountered this message was a little different. Here is an example showing the problem I ran into:<console>:4: error: missing parameter type for expanded function ((x0$1) => x0$1 match { case "x" => println("Got x") }) val nfv = { case "x" => println("Got x") } ^
Calling the above methodclass PF[T] { //partial function type def sub(x:PartialFunction[T,Unit]) = x } class FF[T] { //full function type def sub(x:Function[T,Unit]) = x } class NF[T] { //no unique function type def sub(x:PartialFunction[T,Unit]) = x def sub(x:Function[T,Unit]) = x } val pf = new PF[String] val ff = new FF[String] val nf = new NF[String] pf.sub{ case "x" => println("x") } //works, result is PartialFunction ff.sub{ case "x" => println("x") } //works, result is Function1 nf.sub{ case "x" => println("x") } //fails with compiler error msg
sub
with a case statement
works when there is only one method of that name,
whether it takes a Function1
or a
PartialFunction
, but although the
compiler has no problem compiling the overloaded pair of functions,
once they both exist the compiler can no longer unambiguously determine
the target type for the case statement,
so it delivers that same error message
"missing parameter type for expanded function".
In my case I was trying to modify the
subscribe
method
in my Publisher
class so that I could
pass in either a regular function, such as println(_)
,
or a PartialFunction
, in particular an in-line case statement.
The three options I tried are essentially classes PF, FF and NF listed above.
When I used approach NF I was unable to directly pass in a case statement,
but instead would get the compiler error mentioned above.
When I used approach PF I could pass in a case statement as a
PartialFunction
,
but I could not pass in a regular function.
When I used approach FF I could pass in a regular function, and
could pass in and properly deal with a PartialFunction
,
since it extends Function1
, but
when I used an in-line case statement it would get
compiled as a Function1
rather than a PartialFunction
, which would cause
execution to fail when a value was passed to that case statement that
it did not cover (since it was not a PartialFunction
and
thus did not have an isDefinedAt
method to call first).
I don't like option FF because it would allow code (specifically, an in-line case statement) to compile but then not execute as expected. Options PF and NF are not very useful as is, since neither directly supports both case statements and full functions.
In a mailing list response to someone who was attempting to use option NF in his application, Paul Phillips suggested using option FF with a helper function
pf
that accepts a PartialFunction
and returns the same value, then wrapping any case statements inside a
call to that helper function;
or, alternatively, assigning the case statement to a
val
declared as
a PartialFunction
before passing it to method sub
.
Unfortunately, if the user forgets to use either of these techniques on
a case statement and just passes it directly to method
sub
in option FF,
it will be handled as a Function1
rather than a
PartialFunction
, so
it will compile but not behave as expected.
Paul's suggestion would also work in option NF (and in option PF, although in that case it would be redundant), which would behave much the same as option FF from the user's perspective except that passing a bare case statement to the overloaded method
sub
would not compile, so we would no longer have the undesirable situation
of something that compiles but behaves unexpectedly.
As an alternative to Paul's
pf
helper function, I could
write a helper function ff
that takes a Function1
and turns it into a
PartialFunction
with an isDefinedAt
method that always
returns true.
I would then use this with option PF.
This would allow me to directly pass in case statements, but I would
have to wrap all regular functions in a call to ff
.
I have not yet made any changes to my
Publisher
class,
since I don't particularly like either of the options
and I don't currently really need the ability to use in-line case statements.
Meanwhile, if
I get the compiler error "missing parameter type for expanded function"
while trying to use an in-line case statement,
at least I now know one more thing to check for.
9 comments:
You could also just define an implicit to convert any total function into an equivalent partial function:
implicit def toPartialFunction[A,B] ( x: (A)=>B ) = new PartialFunction[A,B]{
def apply(value:A) = x(value)
def isDefinedAt(value:A) = true
}
Then define your publisher class on a partial function, and let the Scala compiler save you the keyboarding.
Ken B: That works if the type of the total function is fully defined, but not when the function type is incompletely specified. I want to be able to use the "_" shorthand for my callbacks and not have to fully specify the types of everything; for example, I want to be able to use "println(_)" rather than having to use "(s:String)=>println(s)" or "println(_:String)". "val x:Function1[String,Unit] = println(_)" works because Scala knows that "println(_)" must be of type "Function1[String,Unit]", but "println(_)" does not have enough type information for Scala to match it against an implicit conversion function. If you can make the statement "val x:PartialFunction[String,Unit] = println(_)" compile by using implicit conversions, I would be interested in seeing how that can be done.
Hi Jim,
I've been bitten by this issue recently and trying to revisit it, I'm wondering if one of the way out would be to do this:
class ExtendedFunction[T, S](f: Function[T, S]) {
def applySafely(t: T): Option[S] = try {
Some(f.apply(t))
} catch { case e: MatchError => None }
}
implicit def toExtendedFunction[T, S](f: Function[T, S]) = new ExtendedFunction(f: Function[T, S])
class Test[T](val t: T) {
def sub(x:Function[T, Unit]) = x.applySafely(t)
}
val test = new Test("hello")
test sub { println(_) }
test sub { case s => println("partial " + s) }
A bit ugly but at least from the api user point of view that behaves as expected right?
Eric.
Hmm, too good to be true. There a nasty situation with what I described. If the function succeeds in applying its case statement but the right hand side of the case statement raises a MatchError, then there is no way to distinguish the two.
I'm sorry, it's like I'm polluting your blog here but I think I may have a solution,...
If I define applySafely as:
def applySafely(a: A): Option[B] = {
try {
Some(f(a))
} catch {
case e: MatchError => {
if (e.getCause() == null)
None
else
throw e
}
}
}
then, this additional check for getCause does the trick. When the function, which we want to use as a partial function, fails because of a second match getCause won't be null. So:
val f2: Function[Int, String] = { case a => a match { case x if x > 0 => x.toString } }
f2.applySafely(0) will throw an exception as expected.
Eric: In a simple test I tried, getCause was null even when thrown from the nested match expression (which I believe is the correct behavior, since nothing in that code is catching and rethrowing), so I don't think that test works to distinguish the two potential causes of MatchError. But your approach, while I can't say it is elegant, is interesting, and it might be possible to make it work somehow.
You're right, I was actually misled by the fact that my specification is passing as expected: http://gist.github.com/411399
And I still don't understand why it works (using Scala 2.8.0.RC2).
If you have any idea,... Thanks.
Eric: your spec test on f2 is passing because you are calling apply rather than applySafely.
OMG, I can't believe I'm that blind. For my defense I was sick in bed yesterday,... So we're left with an applySafely that would swallow *any* MatchError.
Thanks for your patience and your posts!
Post a Comment