Vraag Scala: korte vorm van patroonvergelijking die Boolean retourneert


Ik merkte dat ik zoiets vaak schreef:

a match {     
  case `b` => // do stuff
  case _ => // do nothing
}

Is er een kortere manier om te controleren of een waarde overeenkomt met een patroon? Ik bedoel, in dit geval zou ik gewoon kunnen schrijven if (a == b) // do stuff, maar wat als het patroon complexer is? Zoals bij het matchen met een lijst of een patroon van willekeurige complexiteit. Ik zou graag zoiets als dit kunnen schrijven:

if (a matches b) // do stuff

Ik ben relatief nieuw voor Scala, dus vergeef me als ik iets groots mis :)


40
2017-12-14 08:05


oorsprong


antwoorden:


Dit is precies waarom ik deze functies heb geschreven, die blijkbaar indrukwekkend onbekend zijn omdat niemand ze heeft genoemd.

scala> import PartialFunction._
import PartialFunction._

scala> cond("abc") { case "def" => true }
res0: Boolean = false

scala> condOpt("abc") { case x if x.length == 3 => x + x }
res1: Option[java.lang.String] = Some(abcabc)

scala> condOpt("abc") { case x if x.length == 4 => x + x }
res2: Option[java.lang.String] = None

58
2017-12-14 17:40



De match operator in Scala is het krachtigst wanneer het wordt gebruikt in functionele stijl. Dit betekent, in plaats van "iets" te doen in de case verklaringen, zou u een bruikbare waarde teruggeven. Hier is een voorbeeld voor een imperatieve stijl:

var value:Int = 23
val command:String = ... // we get this from somewhere
command match {
  case "duplicate" => value = value * 2
  case "negate" => value = -value
  case "increment" => value = value + 1
  // etc.
  case _ => // do nothing
}
println("Result: " + value)

Het is heel begrijpelijk dat het "niets doen" een beetje pijn doet, omdat het overvloeiend lijkt. Dit is echter te wijten aan het feit dat het bovenstaande in imperatieve stijl is geschreven. Hoewel constructies als deze soms nodig kunnen zijn, kunt u in veel gevallen uw code refunteren naar functionele stijl:

val value:Int = 23
val command:String = ... // we get this from somewhere
val result:Int = command match {
   case "duplicate" => value * 2
   case "negate" => -value
   case "increment" => value + 1
   // etc.
   case _ => value
}
println("Result: " + result)

In dit geval gebruik je het geheel match statement als een waarde die u bijvoorbeeld aan een variabele kunt toewijzen. En het is ook veel meer voor de hand liggend dat het match statement moet in elk geval een waarde teruggeven; als de laatste case zou ontbreken, kon de compiler niet zomaar iets verzinnen.

Het is een kwestie van smaak, maar sommige ontwikkelaars beschouwen deze stijl als transparanter en gemakkelijker om te hanteren in realistischere voorbeelden. Ik durf te wedden dat de uitvinders van de Scala-programmeertaal een meer functioneel gebruik voor ogen hadden match, en inderdaad de if verklaring is logischer als u alleen hoeft te beslissen of een bepaalde actie al dan niet moet worden ondernomen. (Aan de andere kant kunt u ook gebruiken if op de functionele manier, omdat het ook een retourwaarde heeft ...)


12
2017-12-14 09:30



Dit kan helpen:

class Matches(m: Any) {
    def matches[R](f: PartialFunction[Any, R]) { if (f.isDefinedAt(m)) f(m) }
}
implicit def any2matches(m: Any) = new Matches(m)

scala> 'c' matches { case x: Int => println("Int") }                                

scala> 2 matches { case x: Int => println("Int") }  
Int

Nu, enige uitleg over de algemene aard van het probleem.

Waar kan een match gebeuren?

Er zijn drie plaatsen waar patroonafstemming kan optreden: val, case en for. De regels voor hen zijn:

// throws an exception if it fails
val pattern = value 

// filters for pattern, but pattern cannot be "identifier: Type",
// though that can be replaced by "id1 @ (id2: Type)" for the same effect
for (pattern <- object providing map/flatMap/filter/withFilter/foreach) ...

// throws an exception if none of the cases match
value match { case ... => ... }

Er is echter nog een situatie waarin case zou kunnen verschijnen, wat letterlijke functie- en partiële functies zijn. Bijvoorbeeld:

val f: Any => Unit = { case i: Int => println(i) }
val pf: PartialFunction[Any, Unit] = { case i: Int => println(i) }

Zowel functies als gedeeltelijke functies werpen een uitzondering als ze worden aangeroepen met een argument dat niet overeenkomt met een van de case-instructies. Gedeeltelijke functies bieden echter ook een methode genaamd isDefinedAt die kan testen of een overeenkomst kan worden gemaakt of niet, evenals een methode genaamd lift, die een a zal worden PartialFunction[T, R] in een Function[T, Option[R]], wat betekent dat niet-overeenkomende waarden resulteren in None in plaats van een uitzondering te geven.

Wat is een match?

Een match is een combinatie van veel verschillende tests:

// assign anything to x
case x

// only accepts values of type X
case x: X

// only accepts values matches by pattern
case x @ pattern

// only accepts a value equal to the value X (upper case here makes a difference)
case X

// only accepts a value equal to the value of x
case `x`

// only accept a tuple of the same arity
case (x, y, ..., z)

// only accepts if extractor(value) returns true of Some(Seq()) (some empty sequence)
case extractor()

// only accepts if extractor(value) returns Some something
case extractor(x)

// only accepts if extractor(value) returns Some Seq or Tuple of the same arity
case extractor(x, y, ...,  z)

// only accepts if extractor(value) returns Some Tuple2 or Some Seq with arity 2
case x extractor y

// accepts if any of the patterns is accepted (patterns may not contain assignable identifiers)
case x | y | ... | z

Nu zijn extractors de methoden unapply of unapplySeq, de eerste keer terug Boolean of Option[T]en de tweede keer terug Option[Seq[T]], waar None betekent dat er geen overeenkomst is gemaakt, en Some(result) zal proberen te evenaren result zoals hierboven beschreven.

Er zijn dus allerlei syntactische alternatieven, die gewoon niet mogelijk zijn zonder het gebruik van een van de drie constructies waar patroonovereenkomsten kunnen voorkomen. Mogelijk kunt u sommige functies emuleren, zoals waardegelijkheid en extractors, maar niet allemaal.


12
2017-12-14 14:31



Patronen kunnen ook worden gebruikt voor uitdrukkingen. Uw codevoorbeeld

a match {     
  case b => // do stuff
  case _ => // do nothing
}

kan vervolgens worden uitgedrukt als

for(b <- Some(a)) //do stuff

De truc is om een ​​wrap te maken om er een geldige opgave van te maken. Bijv. Lijst (a) zou ook werken, maar ik denk dat Sommige (a) het dichtst bij de door u beoogde betekenis ligt.


7
2017-12-14 12:19



Het beste wat ik kan bedenken is dit:

def matches[A](a:A)(f:PartialFunction[A, Unit]) = f.isDefinedAt(a)

if (matches(a){case ... =>}) {
    //do stuff
}

Dit zal je echter geen stijlpunten opleveren.


5
2017-12-14 09:04



Kim's antwoord kan worden "verbeterd" om beter aan uw vereisten te voldoen:

class AnyWrapper[A](wrapped: A) {
  def matches(f: PartialFunction[A, Unit]) = f.isDefinedAt(wrapped)
}
implicit def any2wrapper[A](wrapped: A) = new AnyWrapper(wrapped)

dan:

val a = "a" :: Nil
if (a matches { case "a" :: Nil => }) {
  println("match")
}

Ik zou het echter niet doen. De => }) { volgorde is hier echt lelijk, en de hele code ziet er veel minder duidelijk uit dan een normale overeenkomst. Bovendien krijg je de compile-time overhead van het opzoeken van de impliciete conversie en de runtime-overhead van het inpakken van de wedstrijd in een PartialFunction (afgezien van de conflicten die je zou kunnen krijgen met andere, al gedefinieerd matches methoden, zoals die in String).

Om er een beetje beter uit te zien (en minder uitgebreid te zijn), zou je dit kunnen toevoegen aan AnyWrapper:

def ifMatch(f: PartialFunction[A, Unit]): Unit = if (f.isDefinedAt(wrapped)) f(wrapped)

en gebruik het als volgt:

a ifMatch { case "a" :: Nil => println("match") }

wat je je bespaart case _ => regel, maar vereist dubbele accolades als je een blok wilt in plaats van een enkele instructie ... Niet zo leuk.

Merk op dat dit construct niet echt in de geest van functioneel programmeren is, omdat het alleen kan worden gebruikt om iets uit te voeren dat bijwerkingen heeft. We kunnen het niet gemakkelijk gebruiken om een ​​waarde terug te geven (daarom de Unit retourwaarde), omdat de functie gedeeltelijk is - we hebben een standaardwaarde nodig, of we kunnen een Option aanleg. Maar nogmaals, we zouden het waarschijnlijk uitpakken met een lucifer, dus we zouden niets winnen.

Eerlijk gezegd kun je er maar beter aan wennen deze te zien en te gebruiken match vaak, en afstand nemen van dit soort imperatief-achtige constructies (volgt De leuke uitleg van Madoc).


3
2017-12-14 09:35