Dotty: Boolean extractors force awkward @-pattern syntax

Created on 5 Mar 2019  Â·  11Comments  Â·  Source: lampepfl/dotty

Currently boolean extractors of the form,

object Even {
  def unapply(s: String): Boolean = s.size % 2 == 0
}

can only be bound in matches by using @-patterns,

"even" match {
  case s @ Even() => println(s"$s has an even number of characters")
  case s          => println(s"$s has an odd number of characters")
}

It would be a great deal more natural to use normal binding syntax, ie.,

"even" match {
  case Even(s) => println(s"$s has an even number of characters")
  case s       => println(s"$s has an odd number of characters")
}
pattern-matching

Most helpful comment

@milessabin as far as I understand, Boolean extractors were created specifically with the intention of covering 0-ary pattern matching (which means this particular syntax with empty parens). They are appropriate when you don't need to actually "extract" anything from the value being matched. So from my point of view it just looks like the wrong tool for your use case.

All 11 comments

Here's a version which does what you want while also avoiding any Option-related runtime boxing:

class Even(private val s: String) extends AnyVal {
  def isEmpty: Boolean = s.size % 2 != 0
  def get: String = s
}
object Even {
  def unapply(s: String): Even = new Even(s)
}

~@ghik that's nice, but in the real case I can't add methods to the equivalent of your Even~.

Actually, that's not a problem, but I take your suggestion as a workaround rather than an optimal rendering of the original problem.

To me in proposed syntax looks like s is the result of some sort of extracted/decomposition performed by Even. I would rather direct people to the case ... if ... => syntax which makes it really explicit that the case is guarded by a Boolean predicate and that there is no transformation on the scrutinee:

def even(s: String): Boolean = s.size % 2 == 0

"even" match {
  case s if even(x) => println(s"$s has an even number of characters")
  case s            => println(s"$s has an odd number of characters")
}

@milessabin as far as I understand, Boolean extractors were created specifically with the intention of covering 0-ary pattern matching (which means this particular syntax with empty parens). They are appropriate when you don't need to actually "extract" anything from the value being matched. So from my point of view it just looks like the wrong tool for your use case.

I concur with @ghik. The use-site you want is covered by 1-ary extractors, which are returning Option[T]. A Boolean extractor is a 0-ary extractor.

Nothing should change here. The current behavior is correct and intended.

OK, I'm not going to flog a dead horse here, but I will note that extractors of the form,

object Even {
  def unapply(x: Int): Option[Int] = if(x%2 == 0) Some(x) else None
}

are canonical, and seen quite often in the wild. It would be nice to be able to offer a non-boxing alternative using boolean extractors like so,

object Even {
  def unapply(x: Int): Boolean = x%2 == 0
}

without forcing people to rewrite all the corresponding cases into @-pattern or guarded form.

Granted this _can_ be done using the encoding suggested by @ghik, but it's a hell of a lot clunkier than than the above.

@milessabin for less clunky solution, all we need is a value class version of Option. This is totally doable, see e.g. Opt or NOpt. Note that these implementations have no problem with nesting (Opt(Opt.Empty) is totally distinguishable from Opt.Empty).

Then your extractor is just:

object Even {
  def unapply(x: Int): Opt[Int] = if(x%2 == 0) Opt(x) else Opt.Empty
}

Int will be boxed into a java.lang.Integer but there will be no boxing associated with Opt itself. In this situation, Option boxes twice! If we had a reference type instead of an Int, Opt would not cause any boxing at all.

It would be nice if the standard library had the & extractor that I've
written for myself here and there:

object & {
def unapplyA = Some((a, a))
}

It lets you use any number of patterns in the same match. For instance:

case Method("POST") & PathEnds(".xml") =>

(to completely make up two patterns you might want to match on the same
object, in this case an http request)

It's a bit more elegant than @ for this because it's general.

On Tue, Mar 5, 2019, 10:36 AM Roman Janusz notifications@github.com wrote:

@milessabin https://github.com/milessabin for less clunky solution, all
we need is a value class version of Option. This is totally doable, see
e.g. Opt
https://github.com/AVSystem/scala-commons/blob/master/commons-core/src/main/scala/com/avsystem/commons/misc/Opt.scala
or NOpt
https://github.com/AVSystem/scala-commons/blob/master/commons-core/src/main/scala/com/avsystem/commons/misc/NOpt.scala.
Note that these implementations have no problem with nesting (
Opt(Opt.Empty) is totally distinguishable from Opt.Empty).

Then your extractor is just:

object Even {
def unapply(x: Int): Opt[Int] = if(x%2 == 0) Opt(x) else Opt.Empty
}

Int will be boxed into a java.lang.Integer but there will be no boxing
associated with Opt itself. In this situation, Option boxes twice!

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/lampepfl/dotty/issues/6021#issuecomment-469726662,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAGAUFRaYQtG8b9K7jYkhcdZkffPD6Chks5vTo74gaJpZM4bejCZ
.

I'll beat the equine over at https://github.com/scala/bug/issues/9836 which deals with arity-1 patterns.

When Paul was ironing out the behavior of boolean extractors under NBPM (name-based pattern matching, was it never an initialism? or "namby-pamby"), there was a thread about encodings. The degrees of freedom include isEmpty, the type of get, and the type of _1 where there is no _2.

Although I object to the phrase, "normal binding syntax", there may be uses for the alternative. For example,

scala> object Even { def unapply(s: String) = s.size % 2 == 0 }
defined object Even

scala> object Split { def unapply(s: String) = Option((s.take(s.size/2),s.drop(s.size/2))) }
defined object Split

scala> "abcd" match { case Split(a,b) => s"$a/$b" }
res2: String = ab/cd

scala> "abcd" match { case Even(Split(a,b)) => s"$a/$b" }     // sigh
                           ^
       error: too many patterns for object Even offering Boolean: expected 0, found 1

scala> object Evenly { def unapply(s: String) = Option(s).filter(_.size % 2 == 0) }
defined object Evenly

scala> "abcd" match { case Evenly(Split(a,b)) => s"$a/$b" }
res4: String = ab/cd

This is analogous to the example on the linked ticket, where the first match is perfectly fine (the extractor offers a Tuple2, which can be usefully consumed by a single pattern, noting that the abnormal binding syntax with @ is already accepted):

object X { def unapply(s: String): Option[(Int,Int)] = { val Array(a,b) = s.split("/") ; Some((a.toInt,b.toInt)) }}
object T { def unapply(t: (Int,Int)) = Option(t._1+t._2) }

trait Test {
  "1/2" match { case X(T(x)) => x }

  "1/3" match { case X(x @ (_, _)) => x }

  "1/4" match { case X(x) => x }
}

Namby-Pamby is your Guide;
Albion's Joy, Hibernia's Pride.

Discussions at the SIP meeting tended towards looking at more efficient implementations of Option instead.

A related use case: I have an extractor for X which I would like to extend as a Boolean extractor. I can't specialize my _1 to Boolean (even if that were taken to mean boolean extraction) but I could make it Nothing. The counter-argument is that the new extractor can just delegate and return a Boolean. But there is some elegance in the equivalent of Option[Nothing] serving to mean, you can test for a match, but not get a value out of it.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

m-sp picture m-sp  Â·  3Comments

adamgfraser picture adamgfraser  Â·  3Comments

dwijnand picture dwijnand  Â·  3Comments

fommil picture fommil  Â·  3Comments

noti0na1 picture noti0na1  Â·  3Comments