Spec-Zone.ru › Ruby 3.4

Сопоставление с образцом

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

Сопоставление с образцом в Ruby реализовано с помощью выражения case/in:

case <expression>
in <pattern1>
  ...
in <pattern2>
  ...
in <pattern3>
  ...
else
  ...
end

(Обратите внимание, что ветви in и when НЕ могут быть смешаны в одном выражении case.)

Или с оператором => и оператором in, которые могут быть использованы в отдельном выражении:

<expression> => <pattern>

<expression> in <pattern>

Выражение case/in является полным: если значение выражения не соответствует ни одной ветви выражения case (и ветвь else отсутствует), NoMatchingPatternError генерируется.

Поэтому выражение case может использоваться для условного сопоставления и распаковки:

config = {db: {user: 'admin', password: 'abc123'}}

case config
in db: {user:} # matches subhash and puts matched value in variable user
  puts "Connect with user '#{user}'"
in connection: {username: }
  puts "Connect with user '#{username}'"
else
  puts "Unrecognized structure of config"
end
# Prints: "Connect with user 'admin'"

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

config = {db: {user: 'admin', password: 'abc123'}}

config => {db: {user:}} # will raise if the config's structure is unexpected

puts "Connect with user '#{user}'"
# Prints: "Connect with user 'admin'"

<expression> in <pattern> эквивалентно case <expression>; in <pattern>; true; else false; end. Его можно использовать, когда вам нужно только узнать, было ли выполнено сопоставление с образцом или нет:

users = [{name: "Alice", age: 12}, {name: "Bob", age: 23}]
users.any? {|user| user in {name: /B/, age: 20..} } #=> true

Ниже приведены дополнительные примеры и объяснения синтаксиса.

Образцы

Образцы могут быть:

  • любой объект Ruby (сопоставляется с оператором ===, как в when); (Образец значения)

  • образец массива: [<subpattern>, <subpattern>, <subpattern>, ...]; (Образец массива)

  • образец поиска: [*variable, <subpattern>, <subpattern>, <subpattern>, ..., *variable]; (Образец поиска)

  • образец хэша: {key: <subpattern>, key: <subpattern>, ...}; (Образец хэша)

  • комбинация образцов с |; (Альтернативный образец)

  • захват переменной: <pattern> => variable или variable; (Образец как, Образец переменной)

Любой образец может быть вложен в образцы массивов/поиска/хэшей, где указан <subpattern>.

Array образцы и образцы поиска сопоставляют массивы или объекты, которые отвечают на deconstruct (см. ниже об этом последнем). Hash образцы сопоставляют хэши или объекты, которые отвечают на deconstruct_keys (см. ниже об этом последнем). Обратите внимание, что для образцов хэшей поддерживаются только символьные ключи.

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

case [1, 2, 3]
in [Integer, Integer]
  "matched"
else
  "not matched"
end
#=> "not matched"

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

case {a: 1, b: 2, c: 3}
in {a: Integer}
  "matched"
else
  "not matched"
end
#=> "matched"

{} — единственное исключение из этого правила. Оно соответствует только если задан пустой хэш:

case {a: 1, b: 2, c: 3}
in {}
  "matched"
else
  "not matched"
end
#=> "not matched"

case {}
in {}
  "matched"
else
  "not matched"
end
#=> "matched"

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

case {a: 1, b: 2}
in {a: Integer, **nil} # this will not match the pattern having keys other than a:
  "matched a part"
in {a: Integer, b: Integer, **nil}
  "matched a whole"
else
  "not matched"
end
#=> "matched a whole"

И массивы, и хэши поддерживают спецификацию «остатка»:

case [1, 2, 3]
in [Integer, *]
  "matched"
else
  "not matched"
end
#=> "matched"

case {a: 1, b: 2, c: 3}
in {a: Integer, **}
  "matched"
else
  "not matched"
end
#=> "matched"

Скобки вокруг обоих типов образцов можно опустить:

 case [1, 2]
 in Integer, Integer
   "matched"
 else
   "not matched"
 end
 #=> "matched"

 case {a: 1, b: 2, c: 3}
 in a: Integer
   "matched"
 else
   "not matched"
 end
 #=> "matched"

[1, 2] => a, b
[1, 2] in a, b

{a: 1, b: 2, c: 3} => a:
{a: 1, b: 2, c: 3} in a:

Find образец похож на образец массива, но он может использоваться для проверки наличия в данном объекте элементов, соответствующих образцу:

case ["a", 1, "b", "c", 2]
in [*, String, String, *]
  "matched"
else
  "not matched"
end

Привязка переменных

Помимо глубокой проверки структуры, одним из очень важных свойств сопоставления с образцом является привязка сопоставленных частей к локальным переменным. Основной формой привязки является просто указание => variable_name после сопоставленного (под)образца (это может напоминать хранение исключений в локальных переменных в блоке rescue ExceptionClass => var):

case [1, 2]
in Integer => a, Integer
  "matched: #{a}"
else
  "not matched"
end
#=> "matched: 1"

case {a: 1, b: 2, c: 3}
in a: Integer => m
  "matched: #{m}"
else
  "not matched"
end
#=> "matched: 1"

Если не требуется дополнительная проверка, для привязки только части данных к переменной можно использовать более простую форму:

case [1, 2]
in a, Integer
  "matched: #{a}"
else
  "not matched"
end
#=> "matched: 1"

case {a: 1, b: 2, c: 3}
in a: m
  "matched: #{m}"
else
  "not matched"
end
#=> "matched: 1"

Для образцов хэшей существует еще более простая форма: указание только ключа (без любого подобразца) привязывает локальную переменную к имени ключа тоже:

case {a: 1, b: 2, c: 3}
in a:
  "matched: #{a}"
else
  "not matched"
end
#=> "matched: 1"

Binding работает и для вложенных образцов:

case {name: 'John', friends: [{name: 'Jane'}, {name: 'Rajesh'}]}
in name:, friends: [{name: first_friend}, *]
  "matched: #{first_friend}"
else
  "not matched"
end
#=> "matched: Jane"

Часть «остатка» образца также может быть привязана к переменной:

case [1, 2, 3]
in a, *rest
  "matched: #{a}, #{rest}"
else
  "not matched"
end
#=> "matched: 1, [2, 3]"

case {a: 1, b: 2, c: 3}
in a:, **rest
  "matched: #{a}, #{rest}"
else
  "not matched"
end
#=> "matched: 1, {b: 2, c: 3}"

Binding к переменным в настоящее время НЕ работает для альтернативных образцов, соединенных с |:

case {a: 1, b: 2}
in {a: } | Array
  "matched: #{a}"
else
  "not matched"
end
# SyntaxError (illegal variable in alternative pattern (a))

Переменные, начинающиеся с _ — единственные исключения из этого правила:

case {a: 1, b: 2}
in {a: _, b: _foo} | Array
  "matched: #{_}, #{_foo}"
else
  "not matched"
end
# => "matched: 1, 2"

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

Закрепление переменных

Из-за возможности привязки переменных существующую локальную переменную нельзя напрямую использовать как подобразец:

expectation = 18

case [1, 2]
in expectation, *rest
  "matched. expectation was: #{expectation}"
else
  "not matched. expectation was: #{expectation}"
end
# expected: "not matched. expectation was: 18"
# real: "matched. expectation was: 1" -- local variable just rewritten

В этом случае можно использовать оператор закрепления ^, чтобы сказать Ruby «просто используйте это значение как часть образца»:

expectation = 18
case [1, 2]
in ^expectation, *rest
  "matched. expectation was: #{expectation}"
else
  "not matched. expectation was: #{expectation}"
end
#=> "not matched. expectation was: 18"

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

jane = {school: 'high', schools: [{id: 1, level: 'middle'}, {id: 2, level: 'high'}]}
john = {school: 'high', schools: [{id: 1, level: 'middle'}]}

case jane
in school:, schools: [*, {id:, level: ^school}] # select the last school, level should match
  "matched. school: #{id}"
else
  "not matched"
end
#=> "matched. school: 2"

case john # the specified school level is "high", but last school does not match
in school:, schools: [*, {id:, level: ^school}]
  "matched. school: #{id}"
else
  "not matched"
end
#=> "not matched"

Помимо закрепления локальных переменных, вы также можете закреплять экземпляры, глобальные и классовые переменные:

$gvar = 1
class A
  @ivar = 2
  @@cvar = 3
  case [1, 2, 3]
  in ^$gvar, ^@ivar, ^@@cvar
    "matched"
  else
    "not matched"
  end
  #=> "matched"
end

Вы также можете закреплять результат произвольных выражений, используя скобки:

a = 1
b = 2
case 3
in ^(a + b)
  "matched"
else
  "not matched"
end
#=> "matched"

Сопоставление не примитивных объектов: deconstruct и deconstruct_keys

Как уже упоминалось выше, образцы массивов, поиска и хэшей, помимо литеральных массивов и хэшей, будут пытаться сопоставить любой объект, реализующий deconstruct (для образцов массивов/поиска) или deconstruct_keys (для образцов хэшей).

class Point
  def initialize(x, y)
    @x, @y = x, y
  end

  def deconstruct
    puts "deconstruct called"
    [@x, @y]
  end

  def deconstruct_keys(keys)
    puts "deconstruct_keys called with #{keys.inspect}"
    {x: @x, y: @y}
  end
end

case Point.new(1, -2)
in px, Integer  # sub-patterns and variable binding works
  "matched: #{px}"
else
  "not matched"
end
# prints "deconstruct called"
"matched: 1"

case Point.new(1, -2)
in x: 0.. => px
  "matched: #{px}"
else
  "not matched"
end
# prints: deconstruct_keys called with [:x]
#=> "matched: 1"

keys передаются в deconstruct_keys для предоставления возможностей оптимизации в сопоставляемом классе: если вычисление полного представления хэша дорогостоящее, можно вычислить только необходимый подхэш. При использовании образца **rest в качестве значения nil передается как keys:

case Point.new(1, -2)
in x: 0.. => px, **rest
  "matched: #{px}"
else
  "not matched"
end
# prints: deconstruct_keys called with nil
#=> "matched: 1"

Кроме того, при сопоставлении пользовательских классов ожидаемый класс можно указать как часть образца и проверить с помощью ===

class SuperPoint < Point
end

case Point.new(1, -2)
in SuperPoint(x: 0.. => px)
  "matched: #{px}"
else
  "not matched"
end
#=> "not matched"

case SuperPoint.new(1, -2)
in SuperPoint[x: 0.. => px] # [] or () parentheses are allowed
  "matched: #{px}"
else
  "not matched"
end
#=> "matched: 1"

Эти основные и библиотечные классы реализуют деконструкцию:

  • MatchData#deconstruct и MatchData#deconstruct_keys;

  • Time#deconstruct_keys, Date#deconstruct_keys, DateTime#deconstruct_keys.

Блоки условий

if можно использовать для добавления дополнительного условия (блока условия) при сопоставлении с образцом в выражениях case/in. Это условие может использовать привязанные переменные:

case [1, 2]
in a, b if b == a*2
  "matched"
else
  "not matched"
end
#=> "matched"

case [1, 1]
in a, b if b == a*2
  "matched"
else
  "not matched"
end
#=> "not matched"

unless тоже работает:

case [1, 1]
in a, b unless b == a*2
  "matched"
else
  "not matched"
end
#=> "matched"

Обратите внимание, что операторы => и in не могут иметь блок условия. Следующие примеры анализируются как самостоятельное выражение с модификатором if.

[1, 2] in a, b if b == a*2

Приложение A. Синтаксис образцов

Приблизительный синтаксис:

pattern: value_pattern
       | variable_pattern
       | alternative_pattern
       | as_pattern
       | array_pattern
       | find_pattern
       | hash_pattern

value_pattern: literal
             | Constant
             | ^local_variable
             | ^instance_variable
             | ^class_variable
             | ^global_variable
             | ^(expression)

variable_pattern: variable

alternative_pattern: pattern | pattern | ...

as_pattern: pattern => variable

array_pattern: [pattern, ..., *variable]
             | Constant(pattern, ..., *variable)
             | Constant[pattern, ..., *variable]

find_pattern: [*variable, pattern, ..., *variable]
            | Constant(*variable, pattern, ..., *variable)
            | Constant[*variable, pattern, ..., *variable]

hash_pattern: {key: pattern, key:, ..., **variable}
            | Constant(key: pattern, key:, ..., **variable)
            | Constant[key: pattern, key:, ..., **variable]

Приложение B. Некоторые примеры неопределенного поведения

Для обеспечения гибкости оптимизации в будущем спецификация содержит некоторые примеры неопределенного поведения.

Использование переменной в несопоставленном образце:

case [0, 1]
in [a, 2]
  "not matched"
in b
  "matched"
in c
  "not matched"
end
a #=> undefined
c #=> undefined

Количество вызовов deconstruct, deconstruct_keys:

$i = 0
ary = [0]
def ary.deconstruct
  $i += 1
  self
end
case ary
in [0, 1]
  "not matched"
in [0]
  "matched"
end
$i #=> undefined

Ruby Core © 1993–2024 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API