Синглтон-методы в Ruby.
Синглтон-метод — это метод, который определен только для одного объекта.
Код я буду писать не по-рубистски, со скобками и return-ами — чтобы всем было понятно.
Когда я впервые прочитал об этой штуке в 2016 году, я прям офигел. У меня тогда за плечами был только опыт кодинга на C# и Pascal. Поэтому нет ничего странного в том, что я остановился посреди дороги и сказал «а чё, так можно было?».
Смотри сам:
a = "abc"
# Добавлю синглтон-метод twice для строки a:
def a.twice()
return self + self
end
# Теперь я могу вызвать его:
puts(a.twice()) # => abcabc
# А если я попробую вызвать этот метод на другой строке?
b = "abc"
# puts(b.twice()) # => Ошибка NoMethodError
Строку с вызовом я закомментировал. Если уберёшь коммент, то будет ошибка:
index.rb:13:in `<main>': undefined method `twice' for "abc":String (NoMethodError)
Всё потому что синглтон-метод есть только у объекта, который лежит в переменной a. А у других строк его нет, потому что класс String не изменился — его я не трогал (хотя и могу 😏).
Такой прикол: для любого объекта в Ruby можно сделать уникальные методы, не меняя при этом класс.
Не обязательно создавать именно новый метод — можно «на лету» переопределить уже существующий. Вот так ты можешь пошутить над другом (только не на проде плиз):
# Это где-нибудь в глубине проекта.
def get_string()
a = "abc"
# Переопределю метод длины, теперь
# он возвращает неправильное значение.
def a.length()
return 4
end
return a
end
# А это ничего не подозревающий друг
# использует функцию. И пять часов ищет баг.
value = get_string()
puts(value) # => abc
puts(value.length()) # => 4
Это и правда похоже на привычные синглтоны. Синглтон — это когда у класса может быть только один объект. А синглтон-метод — это когда метод есть только у одного объекта.
Как работает.
Методы в Ruby всегда должны быть внутри класса. Поэтому когда ты создаёшь синглтон-метод, для объекта создаётся неявный класс. Этот новый класс как бы «встраивается» между объектом и его реальным классом:
Объяснение я сильно упростил, на самом деле там всё посложнее и поинтереснее.
А вот с терминологией здесь беда. Эти неявные классы как только не называли: «айгенклассы», «обособленные классы», «виртуальные классы», «метаклассы»… И до сих пор называют по-разному. Как-то так получилось, что эта важная часть Ruby была без общепринятого названия до версии 1.9.2. Тогда в язык ввели метод singleton_class
и теперь эти классы называют «классы-синглтоны». Хотя до этого в Ruby уже было понятие синглтона и поди теперь разберись, кто есть кто. Название логичное, но «айгенклассы» мне нравилось больше.
Окей, теперь давай посмотрим на синглтон-класс:
a = "abc"
def a.twice()
return self + self
end
puts(a.class()) # => String
class
. Метод class
вернул нам String
и нет тут никакого синглтона! Я ведь говорил, что синглтон-классы неявные? Просто так до них не достучаться. Поэтому и понадобился тот самый singleton_class
:
a = "abc"
def a.twice()
return self + self
end
puts(a.singleton_class()) #=> #<Class:<String:0x000056448e75bd30>>
singleton_class
возвращает синглтон-класс объекта. До появления singleton_class
единственный способ получить синглтон-класс был таким:
singleton_class = class << "abc"
self
end
Синглтон-класс для объекта создаётся лениво, по требованию. То есть когда мы добавляем синглтон-метод или вызываем singleton_class
.
Открываем синглтон-класс.
Я показал только один из способов создания синглтон-методов — через def
c указанием объекта. Есть и второй — через «открытие» синглтон-класса. Выглядит реально странно:
a = "abc"
class << a
def twice()
return self + self
end
end
puts(a.twice()) # => "abcabc"
Не считая вот этого вот class << "abc"
, это удобный способ определить сразу несколько синглтон-методов. Синтаксису тоже можно найти объяснение: в Ruby есть оператор <<
, который добавляет элемент в конец массива. То есть у него смысл в том, чтобы что-то куда-то добавлять. Да и сложно, наверное, для такой фичи придумать более адекватный синтаксис.
Зачем всё это надо.
Фича прикольная, но зачем это на уровне языка? Просто на синглтон-методах в Ruby держится половина ООП.
# Сделаю класс с двумя методами —
# один будет статический, а другой — экземплярный:
class Empty
# Это метод экземпляра.
def letter()
return "a"
end
# А это статический метод — объявляется через self.
def self.word()
return "word"
end
end
# Создам экземпляр класса и вызову у него метод letter:
empty = Empty.new()
puts(empty.letter()) # => a
# Как и ожидалось, сработало без проблем.
# А теперь попробую на этом же объекте
# вызвать статический метод:
puts(empty.word())
# Появится ошибка:
# index.rb:12:in `<main>': undefined method `word'
# for #<Empty:0x00005c2959b7a5a0> (NoMethodError)
# То же самое произойдёт, если я попытаюсь
# вызвать метод экземпляра как статический:
puts(Empty.letter())
# Строка выше даёт похожую ошибку:
# index.rb:13:in `<main>': undefined method `letter'
# for Empty:Class (NoMethodError).
# А на самом классе я могу вызывать статические методы спокойно:
puts(Empty.word()) # => word
# Заметь, что в обоих случаях ошибка называется NoMethodError.
Empty
с двумя методами — экземплярным и методом класса. В каком-нибудь другом языке я бы получил ошибку типа «Нельзя вызвать статический метод word
на экземпляре класса». А Ruby просто говорит, что метод не найден. Потому что в Ruby экземплярные и статические методы лежат в разных местах:
- экземплярные — внутри класса,
- статические — в классе-синглтоне этого класса (ведь класс — это тоже объект).
Поэтому и выглядит объявление методов класса точь-в-точь как объявление синглтон-методов. Статические методы в Ruby — это синглтон-методы класса.
Я даже могу проверить это — использую метод singleton_methods
, который возвращает список синглтон-методов объекта:
puts(Empty.singleton_methods) # => letter
singleton_methods
. В Ruby ты можешь сделать два метода с одинаковым именем — один экземплярный, а другой статический. И проблем не возникнет, ведь они лежат в разных местах. В C# была бы ошибка компиляции, а в Python остался бы только один из них (тот, что объявлен позднее).
Ненастоящее ООП.
В первой книге о Ruby, что я прочитал, в самом начале шла цитата Юкихиро Мацумото. А за ней — рассказ о том, что Ruby — настоящий ООП язык 💪. В нём всё — полноценный объект и у всего можно вызывать методы.
Кстати, рекомендую эту книжку, «Язык программирования Ruby» Дэвида Флэнагана — она топ.
Мне нравится, что создатели языков стремятся к единообразию. А то в JavaScript вот есть объекты, а есть "number"
, "string"
, "null"
, "undefined"
… И до сих пор в документации TypeScript говорится: «Пожалуйста, не пишите number c большой буквы! Спасибо!». Хорошо хоть что в Ruby не так. Да ведь?
ООП в Ruby и правда классное, мне нравится. Но вот на синглтон-методах оно ломается. Их просто нельзя определить для значений типов Fixnum
и Symbol
. Ну, потому что это примитивные значения, а не ссылки на объекты. И эту разницу ну никак не заметить. Если не попробовать получить синглтон-класс у числа 24, конечно.
А что там у других.
Идея синглтон-методов есть не только в Ruby. Вот есть язык Dylan, в котором есть синглтон-методы. Правда, они сильно отличаются от тех, что в Ruby:
define method double(thing :: singleton("cup"))
"pint"
end method;
format-out(double("cup")); // => "pint"
Тут они создаются не для конкретного объекта, а для значения — если аргумент равен строке "cup"
, то метод выполняется. Это как сопоставление с образцом (точно как в Haskell). Dylan, кстати, на 3 года старше Ruby.
При помощи синглтон-методов можно эмулировать прототипное ООП и сделать JavaScript из Ruby. Вообще, JavaScript такой идеей, как добавление метода к объекту не удивишь — там это повсюду и без всяких синглтон-классов.
В динамических языках синглтон-методы вполне можно сделать руками. Для Perl 5 есть библиотека, которая приносит в него частичку Ruby:
use SingletonMethod qw/define_singleton_methods/;
my $foo = Foo->new(x => "abc");
define_singleton_methods($foo, {
twice => sub {
my $self = shift;
$self->{x} . $self->{x}
},
});
print $foo->twice(); # => 'abcabc'
package Foo;
sub new {
my $class = shift;
return bless { @_ }, $class;
}
В статических языках гораздо больше ограничений. Но в Java анонимные классы тоже сойдут за синглтон-классы:
public class MyClass {
public static void main(String args[]) {
var obj = new Object() {
private String value = "abc";
String twice() {
return this.value + this.value;
}
};
System.out.println(obj.twice()); // => abcabc
}
}
Конец?
Искать параллели в других языках можно долго, и это интересное занятие. Но ведь даже о синглтонах в Ruby я рассказал так мало и осталась куча нераскрытых вопросов. Я просто напишу их здесь. Если будет интересно, можешь открыть интерпретатор Ruby и попробовать. Погуглить — тоже вариант.
- Что будет, если попробовать получить синглтон-класс у числа 24?
- Как на самом деле синглтон-классы встраиваются в иерархию классов?
- Где хранятся переменные класса?
- Существуют ли синглтон-переменные и синглтон-константы?
Если найдешь что-то, чем захочешь поделиться, смело пиши мне на почту.