Массив возможностей: руководство по сопоставлению с образцом 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
переменным в шаблоне в каждом из индексов.
Совпадающие хэши
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
. Он также проверяет соответствие всех значений для каждого ключа. Затем он присваивает значения переменной в хеше.
Соответствующие подмножества
Проверка качества, используемая при сопоставлении с образцом, следует логике ===
.
Несколько шаблонов
-
|
может использоваться для определения нескольких шаблонов для одного блока.
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 вы хотите проанализировать эти данные и отобразить имя со следующими условиями:
- Если имя пользователя существует, верните имя пользователя.
- Если псевдоним, имя и фамилия существуют, вернуть псевдоним, имя и фамилию.
- Если псевдоним не существует, но есть имя и фамилия, вернуть сначала имя, а затем фамилию.
- Если ни одно из условий не применимо, верните «Новый пользователь».
Вот как я бы написал эту программу на 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 чувствовать себя немного более современным и захватывающим. Я хотел бы увидеть, что люди думают об этом и как эта функция будет развиваться в будущем.