Scala: forme courte de correspondance de modèle qui retourne une valeur booléenne

Je me suis retrouvé à écrire quelque chose comme ça assez souvent:

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

Existe-t-il un moyen plus rapide de vérifier si une valeur correspond à un modèle? Je veux dire, dans ce cas, je pourrais juste écrire if (a == b) // do stuff , mais si le motif est plus complexe? Comme lors de la comparaison avec une liste ou un modèle de complexité arbitraire. J’aimerais pouvoir écrire quelque chose comme ceci:

 if (a matches b) // do stuff 

Je suis relativement nouveau à Scala, alors pardonnez-moi, s’il me manque quelque chose de gros 🙂

C’est exactement pour cette raison que j’ai écrit ces fonctions, qui sont apparemment incroyablement obscures puisque personne ne les a mentionnées.

 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.Ssortingng] = Some(abcabc) scala> condOpt("abc") { case x if x.length == 4 => x + x } res2: Option[java.lang.Ssortingng] = None 

L’opérateur de match dans Scala est le plus puissant lorsqu’il est utilisé dans un style fonctionnel. Cela signifie que, plutôt que de “faire quelque chose” dans les instructions de case , vous retournez une valeur utile. Voici un exemple de style impératif:

 var value:Int = 23 val command:Ssortingng = ... // 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) 

Il est très compréhensible que le “ne rien faire” ci-dessus blesse un peu, car il semble superflu. Cependant, cela est dû au fait que ce qui précède est écrit dans un style impératif. Bien que des constructions comme celles-ci soient parfois nécessaires, dans de nombreux cas, vous pouvez modifier votre code en style fonctionnel:

 val value:Int = 23 val command:Ssortingng = ... // 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) 

Dans ce cas, vous utilisez l’instruction de match complète en tant que valeur que vous pouvez, par exemple, affecter à une variable. Et il est également beaucoup plus évident que la déclaration de match doit renvoyer une valeur dans tous les cas; Si le dernier cas manquait, le compilateur ne pourrait pas simplement créer quelque chose.

C’est une question de goût, mais certains développeurs considèrent que ce style est plus transparent et plus facile à gérer dans des exemples plus réels. Je parierais que les inventeurs du langage de programmation Scala avaient une utilisation plus fonctionnelle en termes de match et, en fait, la déclaration if plus de sens si vous avez seulement besoin de décider si une action doit être prise ou non. (D’autre part, vous pouvez également utiliser if de manière fonctionnelle, car il a également une valeur de retour …)

Cela pourrait aider:

 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 

Maintenant, quelques explications sur la nature générale du problème.

Où un match peut-il arriver?

Il y a trois endroits où la correspondance de modèle peut se produire: val , case et for . Les règles pour eux sont les suivantes:

 // 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 ... => ... } 

Il existe toutefois une autre situation où le case peut apparaître, à savoir les littéraux de fonction et de fonction partielle. Par exemple:

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

Les fonctions et les fonctions partielles déclencheront une exception si elles sont appelées avec un argument qui ne correspond à aucune des déclarations de cas. Cependant, les fonctions partielles fournissent également une méthode appelée isDefinedAt qui peut tester si une correspondance peut être faite ou non, ainsi qu’une méthode appelée lift , qui transformera une fonction PartialFunction[T, R] en une Function[T, Option[R]] , ce qui signifie que les valeurs qui ne correspondent pas donneront None au lieu de générer une exception.

Qu’est-ce qu’un match?

Un match est une combinaison de plusieurs tests différents:

 // 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 

Maintenant, les extracteurs sont les méthodes unapply ou unapplySeq , le premier Boolean retournant ou Option[T] , et le second retournant Option[Seq[T]] , où None signifie qu’aucune correspondance n’est faite, et Some(result) tentera de faire correspondre le result comme décrit ci-dessus.

Il existe donc toutes sortes d’alternatives syntaxiques, qui ne sont tout simplement pas possibles sans l’utilisation de l’une des trois constructions où des correspondances peuvent se produire. Vous pouvez émuler certaines des fonctionnalités, telles que l’égalité de valeur et les extracteurs, mais pas tous.

Les motifs peuvent également être utilisés dans les expressions. Votre exemple de code

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

peut alors être exprimé comme

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

L'astuce consiste à envelopper un pour en faire un énumérateur valide. Par exemple, la liste (a) fonctionnerait également, mais je pense que Some (a) est le plus proche de votre signification.

Le mieux que je puisse trouver est ceci:

 def matches[A](a:A)(f:PartialFunction[A, Unit]) = f.isDefinedAt(a) if (matches(a){case ... =>}) { //do stuff } 

Cela ne vous fera pas gagner de points de style.

La réponse de Kim peut être «améliorée» pour mieux correspondre à vos besoins:

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

puis:

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

Je ne le ferais pas cependant. Le => }) { séquence est vraiment moche ici, et le code entier est beaucoup moins clair qu’une correspondance normale. De plus, vous obtenez le temps de compilation nécessaire pour rechercher la conversion implicite et le temps d’exécution PartialFunction envelopper la correspondance dans une fonction PartialFunction (sans compter les conflits que vous pourriez rencontrer avec d’autres méthodes de matches déjà définies, comme celle de Ssortingng ).

Pour être un peu meilleur (et être moins bavard), vous pouvez append cette AnyWrapper à AnyWrapper :

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

et l’utiliser comme ceci:

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

ce qui vous permet d’économiser votre case _ => ligne, mais nécessite des doubles accolades si vous voulez un bloc au lieu d’une seule instruction … Pas si gentil.

Notez que cette construction n’est pas vraiment dans l’esprit de la functional programming, car elle ne peut être utilisée que pour exécuter quelque chose qui a des effets secondaires. Nous ne pouvons pas facilement l’utiliser pour renvoyer une valeur (donc la valeur de retour de l’ Unit ), car la fonction est partielle – nous aurions besoin d’une valeur par défaut, ou nous pourrions retourner une instance d’ Option . Mais là encore, nous déballerions probablement un match, alors nous ne gagnerions rien.

Franchement, vous feriez mieux de vous habituer à voir et utiliser ces match fréquemment, et à vous éloigner de ce type de constructions de style impératif (suite à la belle explication de Madoc ).