Син­гл­тон-ме­то­ды в 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 всегда должны быть внутри класса. Поэтому когда ты создаёшь синглтон-метод, для объекта создаётся неявный класс. Этот новый класс как бы «встраивается» между объектом и его реальным классом:

Синглтон-класс встраивается между ообъектом и его реальным классом
Неявный класс находится в центре UML-диаграммы.

Объяснение я сильно упростил, на самом деле там всё посложнее и поинтереснее.

А вот с терминологией здесь беда. Эти неявные классы как только не называли: «айгенклассы», «обособленные классы», «виртуальные классы», «метаклассы»… И до сих пор называют по-разному. Как-то так получилось, что эта важная часть 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"
Так выглядят синглтон-методы в Dylan.

Тут они создаются не для конкретного объекта, а для значения — если аргумент равен строке "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;
}
Создание синглтон-методов при помощи библиотеки в Perl 5.

В статических языках гораздо больше ограничений. Но в 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
    }
}
Создание экземпляра анонимного класса в Java.

Конец?

Искать параллели в других языках можно долго, и это интересное занятие. Но ведь даже о синглтонах в Ruby я рассказал так мало и осталась куча нераскрытых вопросов. Я просто напишу их здесь. Если будет интересно, можешь открыть интерпретатор Ruby и попробовать. Погуглить — тоже вариант.

  1. Что будет, если попробовать получить синглтон-класс у числа 24?
  2. Как на самом деле синглтон-классы встраиваются в иерархию классов?
  3. Где хранятся переменные класса?
  4. Существуют ли синглтон-переменные и синглтон-константы?

Если найдешь что-то, чем захочешь поделиться, смело пиши мне на почту.

Что по­смот­реть по теме: