Pattern extractors in anonymous functions

478 views
Skip to first unread message

Donny Nadolny

unread,
Mar 8, 2013, 8:44:14 AM3/8/13
to scala-...@googlegroups.com
I'd like some feedback on an idea I have for an improvement to Scala.

Right now you can use pattern extraction in several places. In assignment:
val (str, num) = ("a", 1)

in for comprehensions:
for ((key, value) <- Map("abc" -> 123)) yield (value, key)

in case clauses:
Map("abc" -> 123).map{case (key,value) => (value, key)}

but not in anonymous functions:
Map("a" -> 1, "b" -> 2).map((key, value) => (value, key)) //not allowed right now
case class Person(name: String, num: Int)
List(Person("Donny", 123)).map(Person(name, num) => name + ": " + num) //not allowed right now

I've made a draft SIP document with a proposal for this at https://docs.google.com/document/d/1-wnYufmPJN7RUh_s-1ZnkLyBAAYb6FBvK41TOl-uJdI/edit
There's a proof of concept implementation at https://github.com/dnadolny/scala

Any feedback would be great.

A few questions I have in particular:
- Ban literals, good idea or bad idea?
- It's not source compatible, although there is a way it could be. It seems to be quite rare to hit on the case where there would be problems, so I don't think it's worth it. Thoughts?

Donny Nadolny

unread,
Mar 8, 2013, 8:57:48 AM3/8/13
to scala-...@googlegroups.com
Also, some implementation notes (any help or advice for making a better implementation than my proof of concept would be good). Right now it's implemented by having the parser transform anonymous functions to case clauses: fun(lhs => rhs) becomes fun({case lsh => rhs}). I think this is generally the right way to do it, but my implementation takes snapshots of the scanner's position and jumps around in it rather than really integrating it in to the syntax. I also have a hack to ignore a bunch of files that it has problems with (the type inference and bracket problems below)

Type inference seems a bit weaker when using case clauses rather than normal anonymous functions. For example,
List(1, 2, 3).reduceLeft((a, b) => a + b) //compiles
List(1, 2, 3).reduceLeft{case (a, b) => a + b} //doesn't compile, needs a type annotation:
List(1, 2, 3).reduceLeft[Int]{case (a, b) => a + b} //compiles
This causes some code to not compile after my transformation since it would need type annotations, unless there's some way to fix this.

Finally, I think there might be a syntax change that needs to be made. I didn't put it in the draft because I'm not sure if it's just a problem with my implementation.
When defining functions in an anonymous function, you need extra brackets when using the {case ...} notation rather than the normal function notation:
class ControlContext[A,B,C](val fun: (A => B, Exception => B) => C, val x: A)
new ControlContext((f: Int => Double, e: Exception => Double) => "a", 2) //compiles
but putting exactly that in a case block doesn't compile:
new ControlContext({case (f: Int => Double, e: Exception => Double) => "a"}, 2) //doesn't compile
not only do you need type annotations for this (because of the weaker type inference?), but the two function type annotations "Int => Double" and "Exception => Double" need to be wrapped in brackets:
new ControlContext[Int, Double, String]({case (f: (Int => Double), e: (Exception => Double)) => "a"}, 2) //compiles

These two problems come up in a few dozen places in the scala compiler / library, so I have excluded these from using my change (see "val ignoredFiles" in Parsers.scala for the list). If you comment them out and remove "build" plus all the generated jars, you can see the errors when you build. You can also see the diff for ControlContext.scala and AnnotationInfos.scala for changes that can be made to the source code to work around it.

I'm hoping that these are both fixable with a better implementation of the change.

Lanny Ripple

unread,
Mar 8, 2013, 11:44:45 AM3/8/13
to scala-...@googlegroups.com
Like the spaghetti sauce, it's in there.

  Map("a" -> 1, "b" -> 2).map{ case (k,v) => (v,k) }

  case class Person(name: String, num: Int)
  List(Person("Donny", 123)).map{ case Person(name, num) => name + ": " + num }

Lanny Ripple

unread,
Mar 8, 2013, 11:49:42 AM3/8/13
to scala-...@googlegroups.com
Aha.  Jumped into the editor after only skimming the post.  Sorry about that.

Haoyi Li

unread,
Mar 13, 2013, 9:48:30 PM3/13/13
to Lanny Ripple, scala-...@googlegroups.com
+1. I think this would be very cool if all the edge cases could be worked out; having to arbitrarily swap out parantheses for curly braces and insert a "case" in there always felt kind of   odd, I'd associate curly braces with sequences of statements, and "case" with partial functions, both of which are completely unrelated to the most common case: a total function which produces a single expression.



--
You received this message because you are subscribed to the Google Groups "scala-debate" group.
To unsubscribe from this group and stop receiving emails from it, send an email to scala-debate...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Aleh Aleshka

unread,
Apr 6, 2013, 2:45:03 PM4/6/13
to scala-...@googlegroups.com
Great work Donny! Would love this to be incorporated.
I don't think banning literals is a good idea since it's pretty usual to have such a match in PartialFunctions, btw does this proposal affect in any way syntax for anonymous PFs ?
Disambiguation between overloads is not a big issue imo, it's pretty unconventional to have such overloads. BTW i wonder if we can disambiguate using TupleN extractor explicitly? E.g. printIt(Tuple2(a,b) => "")

One issue with this idea i can foresee is that it appears to be no way to create multiple cases using this syntax, is that something worth considering?

Thanks, Aleh.

Som Snytt

unread,
Apr 6, 2013, 7:50:31 PM4/6/13
to Lanny Ripple, scala-debate
Like Lanny, I thought it was in there.  But I've also just skimmed the proposal, so I'll jump in, too.

I guess the use case is to save a few chars on a "pattern-matching anonymous function" with one case, so that
{ case (x,y) => ??? }
becomes
((x,y) => ???)

or similarly
{ case Person(name, age) => ??? }
and
(Person(name, age) => ???)


The proposal is to convert all function literals to PMAFs, but an alternative is to apply that fallback only if typechecking fails.  (My browser dictionary suggests I change typechecking to henpecking.)

Right now,

scala> Foo(x) => x+1
<console>:1: error: not a legal formal parameter

If you're going to take that, then why not accept

scala> def m(Foo(x)) = x+1

which would desugar to

def m(x0: Foo) = { val Foo(x) = x0; x+1 }

I think the terminology is not "pattern extractors" but simply "patterns".  An "extractor pattern" is Foo(x) for unapply; a "tuple pattern" is a special case.  Is the conversion intended only for those?

You probably don't mean that

scala> val f = (a: String, b: Int) => a + b
f: (String, Int) => String = <function2>

becomes

scala> val g = { case (a: String, b: Int) => a + b }
<console>:9: error: missing parameter type for expanded function

I agree with others that you can't go outlawing literals just because.

This

scala> val (a: String, 1) = ("A",2)
scala.MatchError: (A,2) (of class scala.Tuple2)

is more succinct than

scala> val (a: String, b: Int) = ("A",2); require(b == 1)
java.lang.IllegalArgumentException: requirement failed

In general, I'd echo that special exclusions are a hard sell. 

Is there synergy with Boolean matches?  Another, cleaner effort to eliminate the case keyword.

https://docs.google.com/document/d/1onPrzSqyDpHScc9PS_hpxJwa3FlPtthxw-bAuuEe8uA

There's another research effort to make match a method on Any, so that every language feature reduces to an invocation of match.

To invoke a program from the command line, you'd just say, scala -e "MyApp match myargs".  A true result would produce a zero exit value.

That last bit was just a bit of humor, or as they call it on scala-humor, "humour."




Donny Nadolny

unread,
Apr 7, 2013, 8:27:43 PM4/7/13
to scala-...@googlegroups.com
On Saturday, April 6, 2013 2:45:03 PM UTC-4, Aleh Aleshka wrote:
Great work Donny! Would love this to be incorporated.
I don't think banning literals is a good idea since it's pretty usual to have such a match in PartialFunctions, btw does this proposal affect in any way syntax for anonymous PFs ?

I think you and Som have convinced me, I'll cross out the part about banning literals. I'm not sure what you mean about affecting anonymous PFs, I don't think this change affects the existing syntax.
 
Disambiguation between overloads is not a big issue imo, it's pretty unconventional to have such overloads. BTW i wonder if we can disambiguate using TupleN extractor explicitly? E.g. printIt(Tuple2(a,b) => "")

Using the TupleN would work, although that's equivalent to doing printIt(((a,b)) => ""), but probably a bit more clear.
 

One issue with this idea i can foresee is that it appears to be no way to create multiple cases using this syntax, is that something worth considering?

I was intending this to be a short hand for when you only use one case, when used as a short hand for doing printIt(tuple => {val (first, second) = tuple; /*rest of code*/}). I think when having multiple cases it's easiest to write out the case keyword.
 

Donny Nadolny

unread,
Apr 7, 2013, 8:33:18 PM4/7/13
to scala-...@googlegroups.com, Lanny Ripple
On Saturday, April 6, 2013 7:50:31 PM UTC-4, som-snytt wrote:
Like Lanny, I thought it was in there.  But I've also just skimmed the proposal, so I'll jump in, too.

I guess the use case is to save a few chars on a "pattern-matching anonymous function" with one case, so that
{ case (x,y) => ??? }
becomes
((x,y) => ???)

or similarly
{ case Person(name, age) => ??? }
and
(Person(name, age) => ???)


The proposal is to convert all function literals to PMAFs, but an alternative is to apply that fallback only if typechecking fails.  (My browser dictionary suggests I change typechecking to henpecking.)

I believe that all of these PMAFs will be converted to normal functions anyway. I remember seeing that somewhere in the spec that if you are passing a single case clause to a function that expects a function, it gets translated in to a normal function rather than a partial function:

scala> def takesAFunction(f: String => String) = println(f.isInstanceOf[PartialFunction[String,String]])
takesAFunction: (f: String => String)Unit

scala> takesAFunction(str => str)
false

scala> takesAFunction({ case str => str })
false

scala> val pf: PartialFunction[String, String] = { case str => str }
pf: PartialFunction[String,String] = <function1>

scala> takesAFunction(pf)
true
 

Right now,

scala> Foo(x) => x+1
<console>:1: error: not a legal formal parameter

If you're going to take that, then why not accept

scala> def m(Foo(x)) = x+1

which would desugar to

def m(x0: Foo) = { val Foo(x) = x0; x+1 }

I had briefly considered that, but didn't put it in. It does make sense to allow it - I think what stopped me was that it makes the syntax for parameters fairly different from what it is now. If people like it, I'll add it.
 

I think the terminology is not "pattern extractors" but simply "patterns".  An "extractor pattern" is Foo(x) for unapply; a "tuple pattern" is a special case.  Is the conversion intended only for those?

I wasn't sure what the right terminology was, I'll update it to just "patterns".
 

You probably don't mean that

scala> val f = (a: String, b: Int) => a + b
f: (String, Int) => String = <function2>

becomes

scala> val g = { case (a: String, b: Int) => a + b }
<console>:9: error: missing parameter type for expanded function

I agree with others that you can't go outlawing literals just because.

Ah, I hadn't thought of using it that way, I only saw it as a source of errors. I'll take that part out.
 

This

scala> val (a: String, 1) = ("A",2)
scala.MatchError: (A,2) (of class scala.Tuple2)

is more succinct than

scala> val (a: String, b: Int) = ("A",2); require(b == 1)
java.lang.IllegalArgumentException: requirement failed

In general, I'd echo that special exclusions are a hard sell. 

Is there synergy with Boolean matches?  Another, cleaner effort to eliminate the case keyword.

https://docs.google.com/document/d/1onPrzSqyDpHScc9PS_hpxJwa3FlPtthxw-bAuuEe8uA

I hadn't seen that before. I think they're fairly independent. No synergy, but no conflict either.
 

Aleh Aleshka

unread,
Apr 8, 2013, 8:07:29 PM4/8/13
to scala-...@googlegroups.com, Lanny Ripple
Hi Som


On Sunday, April 7, 2013 2:50:31 AM UTC+3, som-snytt wrote:
Like Lanny, I thought it was in there.  But I've also just skimmed the proposal, so I'll jump in, too.

I guess the use case is to save a few chars on a "pattern-matching anonymous function" with one case, so that
{ case (x,y) => ??? }
becomes
((x,y) => ???)

or similarly
{ case Person(name, age) => ??? }
and
(Person(name, age) => ???)

I believe the use case is a bit different. It has more to do with unifying tuples and multiple arguments methods.
I see far too often people asking why a.zip(b).find((a,b) => ???) doesn't compile  and this proposal looks like a good way to fix this.

Aleh
Reply all
Reply to author
Forward
0 new messages