Un éventail de possibilités : un guide pour la correspondance de motifs Ruby

Publié: 2022-03-11

La correspondance de motifs est la grande nouveauté de Ruby 2.7. Il a été dédié au tronc afin que toute personne intéressée puisse installer Ruby 2.7.0-dev et le vérifier. Veuillez garder à l'esprit qu'aucun de ces éléments n'est finalisé et que l'équipe de développement recherche des commentaires. Si vous en avez, vous pouvez en informer les committers avant la sortie de la fonctionnalité.

J'espère que vous comprendrez ce qu'est le pattern matching et comment l'utiliser dans Ruby après avoir lu cet article.

Qu'est-ce que la correspondance de modèle ?

La correspondance de modèles est une fonctionnalité que l'on trouve couramment dans les langages de programmation fonctionnels. Selon la documentation de Scala, la correspondance de modèles est « un mécanisme permettant de vérifier une valeur par rapport à un modèle. Une correspondance réussie peut également déconstruire une valeur en ses éléments constitutifs.

Cela ne doit pas être confondu avec Regex, la correspondance de chaînes ou la reconnaissance de formes. La correspondance de modèle n'a rien à voir avec la chaîne, mais plutôt avec la structure des données. La première fois que j'ai rencontré le pattern matching, c'était il y a environ deux ans lorsque j'ai essayé Elixir. J'apprenais Elixir et j'essayais de résoudre des algorithmes avec. J'ai comparé ma solution à d'autres et j'ai réalisé qu'ils utilisaient la correspondance de motifs, ce qui rendait leur code beaucoup plus succinct et plus facile à lire.

À cause de cela, le pattern matching m'a vraiment impressionné. Voici à quoi ressemble la correspondance de motifs dans Elixir :

 [a, b, c] = [:hello, "world", 42] a #=> :hello b #=> "world" c #=> 42

L'exemple ci-dessus ressemble beaucoup à une affectation multiple dans Ruby. Cependant, c'est plus que cela. Il vérifie également si les valeurs correspondent ou non :

 [a, b, 42] = [:hello, "world", 42] a #=> :hello b #=> "world"

Dans les exemples ci-dessus, le nombre 42 sur le côté gauche n'est pas une variable assignée. C'est une valeur pour vérifier que le même élément dans cet index particulier correspond à celui du côté droit.

 [a, b, 88] = [:hello, "world", 42] ** (MatchError) no match of right hand side value

Dans cet exemple, au lieu que les valeurs soient affectées, MatchError est déclenché à la place. C'est parce que le nombre 88 ne correspond pas au nombre 42.

Cela fonctionne également avec des cartes (qui sont similaires au hachage dans Ruby):

 %{"name": "Zote", "title": title } = %{"name": "Zote", "title": "The mighty"} title #=> The mighty

L'exemple ci-dessus vérifie que la valeur du name de la clé est Zote et lie la valeur du titre de la clé au title de la variable.

Ce concept fonctionne très bien lorsque la structure des données est complexe. Vous pouvez affecter votre variable et vérifier les valeurs ou les types sur une seule ligne.

De plus, il permet également à un langage typé dynamiquement comme Elixir d'avoir une surcharge de méthode :

 def process(%{"animal" => animal}) do IO.puts("The animal is: #{animal}") end def process(%{"plant" => plant}) do IO.puts("The plant is: #{plant}") end def process(%{"person" => person}) do IO.puts("The person is: #{person}") end

Selon la clé du hachage de l'argument, différentes méthodes sont exécutées.

J'espère que cela vous montre à quel point la correspondance de modèles peut être puissante. Il existe de nombreuses tentatives pour intégrer des correspondances de motifs dans Ruby avec des gemmes telles que noaidi, qo et egison-ruby.

Ruby 2.7 a également sa propre implémentation pas trop différente de ces gemmes, et c'est ainsi que cela se fait actuellement.

Syntaxe de correspondance de modèle Ruby

La correspondance de modèles dans Ruby se fait via une instruction case . Cependant, au lieu d'utiliser l'habituel when , le mot-clé in est utilisé à la place. Il prend également en charge l'utilisation d'instructions if ou unless :

 case [variable or expression] in [pattern] ... in [pattern] if [expression] ... else ... end

L'instruction case peut accepter une variable ou une expression et celle-ci sera comparée aux modèles fournis dans la clause in . Si ou à moins que des instructions peuvent également être fournies après le modèle. La vérification d'égalité ici utilise également === comme l'instruction case normale. Cela signifie que vous pouvez faire correspondre des sous-ensembles et des instances de classes. Voici un exemple d'utilisation :

Tableaux correspondants

 translation = ['th', 'เต้', 'ja', 'テイ'] case translation in ['th', orig_text, 'en', trans_text] puts "English translation: #{orig_text} => #{trans_text}" in ['th', orig_text, 'ja', trans_text] # this will get executed puts "Japanese translation: #{orig_text} => #{trans_text}" end

Dans l'exemple ci-dessus, la translation de la variable est comparée à deux modèles :

['th', orig_text, 'en', trans_text] et ['th', orig_text, 'ja', trans_text] . Ce qu'il fait est de vérifier si les valeurs du modèle correspondent aux valeurs de la variable de translation dans chacun des indices. Si les valeurs correspondent, il affecte les valeurs de la variable de translation aux variables du modèle dans chacun des indices.

Animation de correspondance de motifs Ruby : tableaux correspondants

Hachages correspondants

 translation = {orig_lang: 'th', trans_lang: 'en', orig_txt: 'เต้', trans_txt: 'tae' } case translation in {orig_lang: 'th', trans_lang: 'en', orig_txt: orig_txt, trans_txt: trans_txt} puts "#{orig_txt} => #{trans_txt}" end

Dans l'exemple ci-dessus, la variable de translation est maintenant un hachage. Il est mis en correspondance avec un autre hachage dans la clause in . Ce qui se passe, c'est que l'instruction case vérifie si toutes les clés du modèle correspondent aux clés de la variable de translation . Il vérifie également que toutes les valeurs de chaque clé correspondent. Il attribue ensuite les valeurs à la variable dans le hachage.

Animation de correspondance de motifs Ruby : tableaux correspondants

Sous-ensembles correspondants

Le contrôle de qualité utilisé dans le pattern matching suit la logique de === .

Motifs multiples

  • | peut être utilisé pour définir plusieurs modèles pour un bloc.
 translation = ['th', 'เต้', 'ja', 'テイ'] case array in {orig_lang: 'th', trans_lang: 'ja', orig_txt: orig_txt, trans_txt: trans_txt} | ['th', orig_text, 'ja', trans_text] puts orig_text #=> เต้ puts trans_text #=> テイend

Dans l'exemple ci-dessus, la variable de translation correspond à la fois au {orig_lang: 'th', trans_lang: 'ja', orig_txt: orig_txt, trans_txt: trans_txt} et au ['th', orig_text, 'ja', trans_text] déployer.

Ceci est utile lorsque vous avez des types de structures de données légèrement différents qui représentent la même chose et que vous souhaitez que les deux structures de données exécutent le même bloc de code.

Affectation des flèches

Dans ce cas, => peut être utilisé pour attribuer une valeur correspondante à une variable.

 case ['I am a string', 10] in [Integer, Integer] => a # not reached in [String, Integer] => b puts b #=> ['I am a string', 10] end

Ceci est utile lorsque vous souhaitez vérifier des valeurs à l'intérieur de la structure de données, mais également lier ces valeurs à une variable.

Opérateur de broche

Ici, l'opérateur pin empêche les variables d'être réaffectées.

 case [1,2,2] in [a,a,a] puts a #=> 2 end

Dans l'exemple ci-dessus, la variable a du modèle est mise en correspondance avec 1, 2, puis 2. Elle sera affectée à 1, puis 2, puis à 2. Ce n'est pas une situation idéale si vous voulez vérifier que tous les les valeurs du tableau sont les mêmes.

 case [1,2,2] in [a,^a,^a] # not reached in [a,b,^b] puts a #=> 1 puts b #=> 2 end

Lorsque l'opérateur pin est utilisé, il évalue la variable au lieu de la réaffecter. Dans l'exemple ci-dessus, [1,2,2] ne correspond pas à [a,^a,^a] car dans le premier index, a est affecté à 1. Dans le deuxième et le troisième, a est évalué à 1, mais est apparié contre 2.

Cependant [a,b,^b] correspond à [1,2,2] puisque a est affecté à 1 dans le premier index, b est affecté à 2 dans le deuxième index, puis ^b, qui vaut maintenant 2, est mis en correspondance avec 2 dans le troisième index donc ça passe.

 a = 1 case [2,2] in [^a,^a] #=> not reached in [b,^b] puts b #=> 2 end

Les variables extérieures à l'instruction case peuvent également être utilisées comme indiqué dans l'exemple ci-dessus.

Opérateur de soulignement ( _ )

Le trait de soulignement ( _ ) est utilisé pour ignorer les valeurs. Voyons cela dans quelques exemples :

 case ['this will be ignored',2] in [_,a] puts a #=> 2 end
 case ['a',2] in [_,a] => b puts a #=> 2 Puts b #=> ['a',2] end

Dans les deux exemples ci-dessus, toute valeur qui correspond à _ passe. Dans la deuxième instruction case, l'opérateur => capture également la valeur qui a été ignorée.

Cas d'utilisation pour la correspondance de modèles dans Ruby

Imaginez que vous disposez des données JSON suivantes :

 { nickName: 'Tae' realName: {firstName: 'Noppakun', lastName: 'Wongsrinoppakun'} username: 'tae8838' }

Dans votre projet Ruby, vous souhaitez analyser ces données et afficher le nom avec les conditions suivantes :

  1. Si le nom d'utilisateur existe, renvoyez le nom d'utilisateur.
  2. Si le surnom, le prénom et le nom de famille existent, renvoyez le surnom, le prénom, puis le nom de famille.
  3. Si le surnom n'existe pas, mais que le prénom et le nom existent, retournez le prénom puis le nom de famille.
  4. Si aucune des conditions ne s'applique, renvoyez "Nouvel utilisateur".

Voici comment j'écrirais ce programme en Ruby en ce moment:

 def display_name(name_hash) if name_hash[:username] name_hash[:username] elsif name_hash[:nickname] && name_hash[:realname] && name_hash[:realname][:first] && name_hash[:realname][:last] "#{name_hash[:nickname]} #{name_hash[:realname][:first]} #{name_hash[:realname][:last]}" elsif name_hash[:first] && name_hash[:last] "#{name_hash[:first]} #{name_hash[:last]}" else 'New User' end end

Voyons maintenant à quoi cela ressemble avec la correspondance de modèle :

 def display_name(name_hash) case name_hash in {username: username} username in {nickname: nickname, realname: {first: first, last: last}} "#{nickname} #{first} #{last}" in {first: first, last: last} "#{first} #{last}" else 'New User' end end

La préférence de syntaxe peut être un peu subjective, mais je préfère la version de correspondance de modèle. En effet, la correspondance de modèles nous permet d'écrire le hachage que nous attendons, au lieu de décrire et de vérifier les valeurs du hachage. Cela permet de visualiser plus facilement les données attendues :

 `{nickname: nickname, realname: {first: first, last: last}}`

Au lieu de:

 `name_hash[:nickname] && name_hash[:realname] && name_hash[:realname][:first] && name_hash[:realname][:last]`.

Déconstruire et Deconstruct_keys

Il y a deux nouvelles méthodes spéciales introduites dans Ruby 2.7 : deconstruct et deconstruct_keys . Lorsqu'une instance d'une classe est mise en correspondance avec un tableau ou un hachage, deconstruct ou deconstruct_keys sont appelés, respectivement.

Les résultats de ces méthodes seront utilisés pour comparer les modèles. Voici un exemple:

 class Coordinate attr_accessor :x, :y def initialize(x, y) @x = x @y = y end def deconstruct [@x, @y] end def deconstruct_key {x: @x, y: @y} end end

Le code définit une classe appelée Coordinate . Il a x et y comme attributs. Il a également défini les méthodes deconstruct et deconstruct_keys .

 c = Coordinates.new(32,50) case c in [a,b] pa #=> 32 pb #=> 50 end

Ici, une instance de Coordinate est définie et le modèle est mis en correspondance avec un tableau. Ce qui se passe ici, c'est que Coordinate#deconstruct est appelé et le résultat est utilisé pour correspondre au tableau [a,b] défini dans le modèle.

 case c in {x:, y:} px #=> 32 py #=> 50 end

Dans cet exemple, la même instance de Coordinate est mise en correspondance avec un hachage. Dans ce cas, le résultat Coordinate#deconstruct_keys est utilisé pour établir une correspondance avec le hachage {x: x, y: y} défini dans le modèle.

Une fonctionnalité expérimentale passionnante

Ayant d'abord expérimenté la correspondance de modèles dans Elixir, j'avais pensé que cette fonctionnalité pourrait inclure une surcharge de méthode et être implémentée avec une syntaxe qui ne nécessite qu'une seule ligne. Cependant, Ruby n'est pas un langage construit avec la correspondance de modèles à l'esprit, donc c'est compréhensible.

L'utilisation d'une instruction case est probablement une manière très légère d'implémenter cela et n'affecte pas non plus le code existant (à l'exception des méthodes deconstruct et deconstruct_keys ). L'utilisation de l'instruction case est en fait similaire à celle de l'implémentation de Scala de la correspondance de modèles.

Personnellement, je pense que la correspondance de modèles est une nouvelle fonctionnalité intéressante pour les développeurs Ruby. Il a le potentiel de rendre le code beaucoup plus propre et de rendre Ruby un peu plus moderne et excitant. J'aimerais voir ce que les gens en pensent et comment cette fonctionnalité évoluera à l'avenir.