Розділення позиційних і ключових аргументів у Ruby 3.0

Опублікував mame 12-12-2019
Переклав: Andrii Furmanets

Ця стаття пояснює заплановану несумісність ключових аргументів у Ruby 3.0

tl;dr

У Ruby 3.0 позиційні аргументи та ключові аргументи будуть розділені. Ruby 2.7 попереджатиме про поведінку, яка зміниться в Ruby 3.0. Якщо ви бачите такі попередження, вам потрібно оновити свій код:

  • Using the last argument as keyword parameters is deprecated, або
  • Passing the keyword argument as the last hash parameter is deprecated, або
  • Splitting the last argument into positional and keyword parameters is deprecated

У більшості випадків можна уникнути несумісності, додавши оператор double splat. Він явно вказує на передачу ключових аргументів замість об’єкта Hash. Так само можна додати фігурні дужки {}, щоб явно передати об’єкт Hash замість ключових аргументів. Докладніше читайте в розділі «Типові випадки» нижче.

У Ruby 3 метод, який делегує всі аргументи, повинен явно делегувати ключові аргументи на додаток до позиційних. Якщо ви хочете зберегти поведінку делегування з Ruby 2.7 і раніших версій, використовуйте ruby2_keywords. Докладніше див. у розділі «Обробка делегування аргументів».

Типові випадки

Ось найтиповіший випадок. Використовуйте оператор double splat (**) для передачі ключових слів замість Hash.

# Цей метод приймає лише ключовий аргумент
def foo(k: 1)
  p k
end

h = { k: 42 }

# Цей виклик методу передає позиційний аргумент Hash
# У Ruby 2.7: Hash автоматично перетворюється на ключовий аргумент
# У Ruby 3.0: Цей виклик викликає ArgumentError
foo(h)
  # => demo.rb:11: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
  #    demo.rb:2: warning: The called method `foo' is defined here
  #    42

# Щоб зберегти поведінку в Ruby 3.0, використовуйте double splat
foo(**h) #=> 42

Ось інший випадок. Використовуйте фігурні дужки ({}) для явної передачі Hash замість ключових слів.

# Цей метод приймає один позиційний аргумент і решту ключових аргументів
def bar(h, **kwargs)
  p h
end

# Цей виклик передає тільки ключовий аргумент і жодних позиційних
# У Ruby 2.7: Ключове слово перетворюється на позиційний аргумент Hash
# У Ruby 3.0: Цей виклик викликає ArgumentError
bar(k: 42)
  # => demo2.rb:9: warning: Passing the keyword argument as the last hash parameter is deprecated
  #    demo2.rb:2: warning: The called method `bar' is defined here
  #    {:k=>42}

# Щоб зберегти поведінку в Ruby 3.0, використовуйте фігурні дужки
bar({ k: 42 }) # => {:k=>42}

Що застаріло?

У Ruby 2 ключові аргументи можуть оброблятися як останній позиційний аргумент Hash, і навпаки — останній позиційний Hash може оброблятися як ключові аргументи.

Оскільки автоматичне перетворення іноді занадто складне й проблемне (як описано в останньому розділі), воно застаріло в Ruby 2.7 і буде видалено в Ruby 3. Іншими словами, в Ruby 3 ключові аргументи будуть повністю відокремлені від позиційних. Отже, коли ви хочете передати ключові аргументи, завжди використовуйте foo(k: expr) або foo(**expr). Якщо хочете приймати ключові аргументи, в принципі завжди використовуйте def foo(k: default) або def foo(k:) або def foo(**kwargs).

Зверніть увагу, що Ruby 3.0 поводиться так само при виклику методу, який не приймає ключові аргументи, з ключовими аргументами. Наприклад, наступний випадок не буде застарілим і працюватиме в Ruby 3.0. Ключові аргументи все ще обробляються як позиційний аргумент Hash.

def foo(kwargs = {})
  kwargs
end

foo(k: 1) #=> {:k=>1}

Це тому, що цей стиль використовується дуже часто, і немає неоднозначності в тому, як слід обробляти аргумент.

Однак цей стиль не рекомендується в новому коді, якщо ви не часто передаєте Hash як позиційний аргумент і також використовуєте ключові аргументи. В іншому випадку використовуйте double splat:

def foo(**kwargs)
  kwargs
end

foo(k: 1) #=> {:k=>1}

Чи зламається мій код на Ruby 2.7?

Коротка відповідь: «можливо, ні».

Зміни в Ruby 2.7 розроблено як шлях міграції до 3.0. Хоча в принципі Ruby 2.7 лише попереджає про поведінку, яка зміниться в Ruby 3, він включає деякі несумісні зміни, які ми вважаємо незначними. Докладніше див. розділ «Інші незначні зміни».

За винятком попереджень і незначних змін, Ruby 2.7 намагається зберегти сумісність з Ruby 2.6. Отже, ваш код, ймовірно, працюватиме на Ruby 2.7, хоча може видавати попередження. І запустивши його на Ruby 2.7, ви можете перевірити, чи готовий ваш код до Ruby 3.0.

Якщо хочете вимкнути попередження про застаріння, використовуйте аргумент командного рядка -W:no-deprecated або додайте Warning[:deprecated] = false у свій код.

Обробка делегування аргументів

Ruby 2.6 або раніше

У Ruby 2 можна написати метод делегування, приймаючи аргумент *rest і аргумент &block, і передаючи обидва до цільового методу. У цій поведінці ключові аргументи також неявно обробляються автоматичним перетворенням між позиційними та ключовими аргументами.

def foo(*args, &block)
  target(*args, &block)
end

Ruby 3

Потрібно явно делегувати ключові аргументи.

def foo(*args, **kwargs, &block)
  target(*args, **kwargs, &block)
end

Альтернативно, якщо вам не потрібна сумісність з Ruby 2.6 або раніше і ви не змінюєте жодних аргументів, можете використовувати новий синтаксис делегування (...), введений у Ruby 2.7.

def foo(...)
  target(...)
end

Ruby 2.7

Коротко: використовуйте Module#ruby2_keywords і делегуйте *args, &block.

ruby2_keywords def foo(*args, &block)
  target(*args, &block)
end

ruby2_keywords приймає ключові аргументи як останній аргумент Hash і передає їх як ключові аргументи при виклику іншого методу.

Сумісне делегування для Ruby 2.6, 2.7 і Ruby 3

Коротко: знову використовуйте Module#ruby2_keywords.

ruby2_keywords def foo(*args, &block)
  target(*args, &block)
end

На жаль, нам потрібно використовувати старий стиль делегування (тобто без **kwargs), оскільки Ruby 2.6 або раніше не обробляє новий стиль делегування правильно. Це одна з причин розділення ключових аргументів. І ruby2_keywords дозволяє запускати старий стиль навіть у Ruby 2.7 і 3.0. Оскільки ruby2_keywords не визначено в 2.6 або раніше, використовуйте гем ruby2_keywords або визначте його самостійно:

def ruby2_keywords(*)
end if RUBY_VERSION < "2.7"

Якщо вашому коду не потрібно працювати на Ruby 2.6 або старішому, можете спробувати новий стиль у Ruby 2.7. У майже всіх випадках він працює. Однак зверніть увагу на кутові випадки:

def target(*args)
  p args
end

def foo(*args, **kwargs, &block)
  target(*args, **kwargs, &block)
end

foo({})       #=> Ruby 2.7: []   ({} відкидається)
foo({}, **{}) #=> Ruby 2.7: [{}] (Можете передати {} явно вказавши «без» ключових слів)

Порожній аргумент Hash автоматично перетворюється і поглинається в **kwargs, і виклик делегування видаляє порожній keyword hash, тому жодного аргументу не передається в target.

Якщо ви справді турбуєтесь про переносимість, використовуйте ruby2_keywords. ruby2_keywords може бути видалено в майбутньому після того, як Ruby 2.6 досягне кінця життєвого циклу. На той момент ми рекомендуємо явно делегувати ключові аргументи.

Інші незначні зміни

У Ruby 2.7 є три незначні зміни щодо ключових аргументів.

1. Не-Symbol ключі дозволені в ключових аргументах

У Ruby 2.6 або раніше в ключових аргументах дозволялися лише Symbol-ключі. У Ruby 2.7 ключові аргументи можуть використовувати не-Symbol ключі.

def foo(**kwargs)
  kwargs
end
foo("key" => 42)
  #=> Ruby 2.6 або раніше: ArgumentError: wrong number of arguments
  #=> Ruby 2.7 або пізніше: {"key"=>42}

2. Double splat з порожнім hash (**{}) не передає аргументів

У Ruby 2.6 або раніше передача **empty_hash передавала порожній Hash як позиційний аргумент. У Ruby 2.7 або пізніше він не передає жодних аргументів.

def foo(*args)
  args
end

empty_hash = {}
foo(**empty_hash)
  #=> Ruby 2.6 або раніше: [{}]
  #=> Ruby 2.7 або пізніше: []

3. Введено синтаксис без ключових аргументів (**nil)

Можна використовувати **nil у визначенні методу, щоб явно позначити, що метод не приймає ключових аргументів. Виклик таких методів з ключовими аргументами призведе до ArgumentError.

def foo(*args, **nil)
end

foo(k: 1)
  #=> Ruby 2.7 або пізніше: no keywords accepted (ArgumentError)

Чому ми застаріли автоматичне перетворення

Автоматичне перетворення спочатку здавалося гарною ідеєю і добре працювало в багатьох випадках. Однак у нього було занадто багато кутових випадків, і ми отримали багато звітів про помилки щодо цієї поведінки.

Автоматичне перетворення не працює добре, коли метод приймає опціональні позиційні аргументи та ключові аргументи. Деякі люди очікують, що останній об’єкт Hash буде оброблено як позиційний аргумент, а інші очікують, що він буде перетворено на ключові аргументи.

Ось один з найбільш заплутаних випадків:

def foo(x, **kwargs)
  p [x, kwargs]
end

def bar(x=1, **kwargs)
  p [x, kwargs]
end

foo({}) #=> [{}, {}]
bar({}) #=> [1, {}]

bar({}, **{}) #=> очікувано: [{}, {}], фактично: [1, {}]

У Ruby 2, foo({}) передає порожній hash як звичайний аргумент (тобто {} присвоюється x), тоді як bar({}) передає ключовий аргумент (тобто {} присвоюється kwargs). Отже, any_method({}) дуже неоднозначний.

Подяки

Цю статтю люб’язно переглянули (або навіть співавторчо написали) Jeremy Evans і Benoit Daloze.

Історія

  • Оновлено 2019-12-25: У 2.7.0-rc2 повідомлення попередження було трохи змінено, і додано API для придушення попереджень.

Останні новини

Вийшов Ruby 4.0.0

Ми раді повідомити про випуск Ruby 4.0.0. Ruby 4.0 представляє “Ruby Box” та “ZJIT”, а також додає багато покращень.

Опублікував naruse 25-12-2025

Новий вигляд документації Ruby

Слідом за ре-дизайном ruby-lang.org, ми маємо більше новин, щоб відсвяткувати 30-річчя Ruby: docs.ruby-lang.org має повністю новий вигляд завдяки Aliki — новій темі за замовчуванням для...

Опублікував Stan Lo 23-12-2025

Більше новин...