Массив возможностей: руководство по сопоставлению с образцом Ruby

Опубликовано: 2022-03-11

Сопоставление с образцом — это большая новая функция, появившаяся в Ruby 2.7. Он был помещен в магистраль, так что любой, кто заинтересован, может установить Ruby 2.7.0-dev и проверить его. Пожалуйста, имейте в виду, что ни один из них не завершен, и команда разработчиков ищет отзывы, поэтому, если они у вас есть, вы можете сообщить об этом коммиттерам до того, как функция выйдет.

Надеюсь, после прочтения этой статьи вы поймете, что такое сопоставление с образцом и как его использовать в Ruby.

Что такое сопоставление с образцом?

Сопоставление с образцом — это функция, которая обычно встречается в функциональных языках программирования. Согласно документации Scala, сопоставление с образцом — это «механизм проверки значения по образцу. Успешное сопоставление также может разложить значение на составные части».

Это не следует путать с регулярным выражением, сопоставлением строк или распознаванием образов. Сопоставление с образцом не имеет ничего общего со строкой, а со структурой данных. Впервые я столкнулся с сопоставлением шаблонов около двух лет назад, когда пробовал Эликсир. Я изучал Эликсир и пытался решать с ним алгоритмы. Я сравнил свое решение с другими и понял, что они использовали сопоставление с образцом, что сделало их код намного более кратким и легким для чтения.

Из-за этого сопоставление с образцом действительно произвело на меня впечатление. Вот как выглядит сопоставление с образцом в Elixir:

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

Приведенный выше пример очень похож на множественное присваивание в Ruby. Однако это нечто большее. Он также проверяет, совпадают ли значения:

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

В приведенных выше примерах число 42 в левой части не является присваиваемой переменной. Это значение для проверки того, что один и тот же элемент в этом конкретном индексе соответствует элементу в правой части.

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

В этом примере вместо присваиваемых значений вызывается MatchError . Это потому, что число 88 не соответствует числу 42.

Он также работает с картами (что похоже на хэш в Ruby):

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

В приведенном выше примере проверяется, что значением name ключа является Zote , и привязывается значение title ключа к названию переменной.

Эта концепция очень хорошо работает, когда структура данных сложна. Вы можете назначить свою переменную и проверить значения или типы в одной строке.

Кроме того, он также позволяет языкам с динамической типизацией, таким как Elixir, иметь перегрузку методов:

 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

В зависимости от ключа хэша аргумента выполняются разные методы.

Надеюсь, это покажет вам, насколько мощным может быть сопоставление с образцом. Предпринимается множество попыток внедрить сопоставление с образцом в Ruby с помощью таких драгоценных камней, как noaidi, qo и egison-ruby.

Ruby 2.7 также имеет свою собственную реализацию, не слишком отличающуюся от этих драгоценных камней, и именно так это делается в настоящее время.

Синтаксис сопоставления шаблонов Ruby

Сопоставление с образцом в Ruby выполняется с помощью оператора case . Однако вместо обычного выражения when используется ключевое слово in . Он также поддерживает использование операторов if или unless :

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

Оператор case может принимать переменную или выражение, и это будет сопоставляться с шаблонами, предоставленными в предложении in. Если или если операторы также могут быть предоставлены после шаблона. Проверка на равенство здесь также использует === как и в обычном операторе case. Это означает, что вы можете сопоставлять подмножества и экземпляры классов. Вот пример того, как вы его используете:

Соответствующие массивы

 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

В приведенном выше примере translation переменной сопоставляется с двумя шаблонами:

['th', orig_text, 'en', trans_text] и ['th', orig_text, 'ja', trans_text] . Что он делает, так это проверяет, соответствуют ли значения в шаблоне значениям в переменной translation в каждом из индексов. Если значения совпадают, он присваивает значения в переменной translation переменным в шаблоне в каждом из индексов.

Ruby Pattern Matching Animation: сопоставление массивов

Совпадающие хэши

 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

В приведенном выше примере переменная translation теперь является хэшем. Он сопоставляется с другим хешем in предложении in. Что происходит, так это то, что оператор case проверяет, совпадают ли все ключи в шаблоне с ключами в переменной translation . Он также проверяет соответствие всех значений для каждого ключа. Затем он присваивает значения переменной в хеше.

Ruby Pattern Matching Animation: сопоставление массивов

Соответствующие подмножества

Проверка качества, используемая при сопоставлении с образцом, следует логике === .

Несколько шаблонов

  • | может использоваться для определения нескольких шаблонов для одного блока.
 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

В приведенном выше примере переменная translation соответствует как {orig_lang: 'th', trans_lang: 'ja', orig_txt: orig_txt, trans_txt: trans_txt} и хешу ['th', orig_text, 'ja', trans_text] множество.

Это полезно, когда у вас есть немного разные типы структур данных, которые представляют одно и то же, и вы хотите, чтобы обе структуры данных выполняли один и тот же блок кода.

Назначение стрелки

В этом случае => можно использовать для присвоения совпадающего значения переменной.

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

Это полезно, когда вы хотите проверить значения внутри структуры данных, а также привязать эти значения к переменной.

Пин-оператор

Здесь оператор вывода предотвращает переназначение переменных.

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

В приведенном выше примере переменная a в шаблоне сопоставляется с 1, 2, а затем с 2. Ей будет присвоено значение 1, затем 2, затем 2. Это не идеальная ситуация, если вы хотите проверить, что все значения в массиве совпадают.

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

Когда используется оператор вывода, он оценивает переменную, а не переназначает ее. В приведенном выше примере [1,2,2] не соответствует [a,^a,^a], потому что в первом индексе a присваивается значение 1. Во втором и третьем индексе a оценивается как 1, но соответствует 2.

Однако [a,b,^b] соответствует [1,2,2], так как a присваивается 1 в первом индексе, b присваивается 2 во втором индексе, затем ^b, который теперь равен 2, сопоставляется с 2 в третьем индексе, поэтому он проходит.

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

Также можно использовать переменные вне оператора case, как показано в примере выше.

Подчеркивание ( _ ) Оператор

Подчеркивание ( _ ) используется для игнорирования значений. Давайте посмотрим на пару примеров:

 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

В двух приведенных выше примерах любое значение, совпадающее с _ , проходит. Во втором операторе case оператор => также фиксирует значение, которое было проигнорировано.

Примеры использования сопоставления с образцом в Ruby

Представьте, что у вас есть следующие данные JSON:

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

В вашем проекте Ruby вы хотите проанализировать эти данные и отобразить имя со следующими условиями:

  1. Если имя пользователя существует, верните имя пользователя.
  2. Если псевдоним, имя и фамилия существуют, вернуть псевдоним, имя и фамилию.
  3. Если псевдоним не существует, но есть имя и фамилия, вернуть сначала имя, а затем фамилию.
  4. Если ни одно из условий не применимо, верните «Новый пользователь».

Вот как я бы написал эту программу на Ruby прямо сейчас:

 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

Теперь давайте посмотрим, как это выглядит при сопоставлении с образцом:

 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

Синтаксические предпочтения могут быть немного субъективными, но я предпочитаю версию с сопоставлением с образцом. Это связано с тем, что сопоставление с образцом позволяет нам записать ожидаемый хэш вместо описания и проверки значений хеша. Это упрощает визуализацию ожидаемых данных:

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

Вместо:

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

Деконструкция и Deconstruct_keys

В Ruby 2.7 представлены два новых специальных метода: deconstruct и deconstruct_keys . Когда экземпляр класса сопоставляется с массивом или хэшем, вызывается deconstruct или deconstruct_keys соответственно.

Результаты этих методов будут использоваться для сопоставления с шаблонами. Вот пример:

 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

Код определяет класс с именем Coordinate . Он имеет x и y в качестве своих атрибутов. Он также имеет определенные методы deconstruct и deconstruct_keys .

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

Здесь определяется экземпляр Coordinate и шаблон сопоставляется с массивом. Здесь происходит то, что вызывается Coordinate#deconstruct , и результат используется для сопоставления с массивом [a,b] определенным в шаблоне.

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

В этом примере один и тот же экземпляр Coordinate сопоставляется с хешем. В этом случае результат Coordinate#deconstruct_keys используется для сопоставления с хешем {x: x, y: y} определенным в шаблоне.

Захватывающая экспериментальная функция

Впервые испытав сопоставление с образцом в Elixir, я подумал, что эта функция может включать перегрузку методов и реализована с синтаксисом, который требует только одну строку. Однако Ruby — это не язык, созданный с учетом сопоставления с образцом, так что это понятно.

Использование оператора case, вероятно, является очень простым способом реализации этого, а также не влияет на существующий код (кроме методов deconstruct и deconstruct_keys ). Использование оператора case на самом деле похоже на реализацию сопоставления с образцом в Scala.

Лично я думаю, что сопоставление с образцом — захватывающая новая функция для разработчиков Ruby. У него есть потенциал, чтобы сделать код намного чище и заставить Ruby чувствовать себя немного более современным и захватывающим. Я хотел бы увидеть, что люди думают об этом и как эта функция будет развиваться в будущем.