scala / scala3

The Scala 3 compiler, also known as Dotty.
https://dotty.epfl.ch
Apache License 2.0
5.81k stars 1.05k forks source link

Boolean extractors force awkward @-pattern syntax #6021

Closed milessabin closed 5 years ago

milessabin commented 5 years ago

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")
}
ghik commented 5 years ago

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)
}
milessabin commented 5 years ago

~@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.

OlivierBlanvillain commented 5 years ago

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")
}
ghik commented 5 years ago

@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.

sjrd commented 5 years ago

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.

milessabin commented 5 years ago

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.

ghik commented 5 years ago

@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.

nafg commented 5 years ago

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

object & { def unapply[A](a: A) = 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 .

som-snytt commented 5 years ago

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.

odersky commented 5 years ago

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

som-snytt commented 5 years ago

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.