UPD. посмотрите мой новый похожий пост, но еще круче: https://weird-programming.dev/oop/classes-only.html

Для меня объектно-ориентированное программирование означает, что система разделена на объекты. Объект - это просто сущность, которая имеет некоторое состояние и некоторое поведение. Вы можете заставить свой объект что-то делать, отправив ему сообщение в надежде, что он вас поймет.

Из практических соображений в каждом языке есть некоторые примитивы; основные типы данных, которые вы можете использовать для написания своей программы. Хотя Ruby предположительно является чистым объектно-ориентированным языком (все является объектом), тем не менее в нем есть некоторые примитивы.

Например, числа. Они выглядят как объекты, у них есть методы и прочее. Однако что они на самом деле? 2 и 3 - разные экземпляры класса Integer, поэтому предполагается, что они имеют разное состояние. Но что это за состояние? Магия.

Давайте попробуем реализовать числа самостоятельно, без магии. Просто для развлечения.

Основные правила

Итак, я придумал этот набор ограничений:

  1. Мы не можем использовать какие-либо базовые типы, кроме nil, true и false.
  2. Нет Stdlib (да).
  3. Блоки хороши (только для выразительности не помешают).
  4. Можно использовать оператор равенства для объектов. Это необходимо для проверки, указывают ли две указанные ссылки на один и тот же объект.
  5. Правила не применяются к тестам, потому что тесты нужны только для того, чтобы проверить, работает ли что-то должным образом.
  6. Правила не применяются к методу #inspect, поскольку он служит только для демонстрационных целей.

Правило №1 довольно спорное. С одной стороны, мы используем магические примитивы. С другой стороны, я считаю, что каждая программа должна иметь логические выражения, чтобы иметь какое-либо применение. И у нас не может быть логических выражений без «ложных» сущностей (nil и false).

Я считаю, что нам действительно не нужны true и false, потому что мы можем использовать nil для
false и любой объект для true. Однако почему бы и нет? Просто для выразительности.

Идея реализации

Одна из вещей, которые я помню из своего времени в университете, - это наш профессор, показывающий нам способ реализации натуральных чисел в терминах аксиом Пеано во время лекции по теории множеств.

По сути, нам нужно:

  1. Некоторая базовая сущность (она будет представлять ноль в наборе натуральных чисел).
  2. Некоторая функция next(x), которая возвращает число после x.

В теории множеств мы можем использовать:

  1. Пустой набор []
  2. Функция, которая возвращает набор из 1 элемента, содержащий его аргумент:
    next(s) = [s]

Итак, наши натуральные числа представлены как:

  • 0 = []
  • 1 = [[]]
  • 2 = [[[]]]

Проблема в том, что в нашем распоряжении нет наборов. Вместо них мы можем использовать списки.

Список - наша основная структура данных

Как видите, я реализовал List как пару. Первый элемент - это какой-то объект, а второй - какой-то другой список. Обратите внимание, что списки неизменяемы.

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

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

  • 0 = ()
  • 1 = (())

Натуральные числа

Каждый числовой объект будет иметь представление внутреннего списка как состояние. Итак,
минимум выглядит так:

Но нам это ни к чему. Мы ничего не можем с этим поделать. И что со всеми этими new(…)? Это совершенно непрактично!

Чтобы сделать этот класс полезным, нам нужно определить некоторое поведение.

Методы

Перед тем, как мы начнем, некоторые утилиты

Давайте добавим в List несколько вспомогательных методов:

Это пригодится позже.

Также нам нужен доступ к другим представлениям списков:

class NaturalNumber
  protected
  attr_reader :list_representation
end

Это интересный момент. Не все знают, что protected в Ruby
отличается от Java. В Java (и некоторых других языках) защищенные методы доступны только дочерним классам.

В Ruby protected означает, что это сообщение может быть отправлено от объекта
того же класса:

Дополнение

Если задуматься, каждое фактическое число равно уровню вложенности пустого списка. Для 0 уровень вложенности равен нулю, 1 оборачивает пустой список один раз, 2 делает это дважды и так далее. Итак, чтобы сложить два числа, нам просто нужно увеличить уровень вложенности одного из них на уровень вложенности другого:

Умножение

Что значит n * 5? Это означает, что мы складываем n с собой пять
раз. У нас уже есть оператор плюса. Давайте воспользуемся этим:

Операторы сравнения

Опять же, нам просто нужно сравнить уровни вложенности:

Остальные операторы можно определить в терминах основных:

Вычитание

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

использование

Хорошо, теперь мы определили основные операции. Что дальше? Как мы их используем? Я не хочу инициализировать числа списками каждый раз, когда они мне нужны.

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

two = NaturalNumber::ONE + NaturalNumber::ONE
three = two + NaturalNumber::ONE
# Don't forget we have multiplication as well!
fifty_four = three * three * three * two

Как видите, для одного и того же номера может быть несколько экземпляров. Но это нормально, потому что они эквивалентны с точки зрения использования.

Все в порядке. У нас есть натуральные числа, и мы даже можем с ними посчитать. Но что с того? Нам нужны целые числа!

Целые числа

Целые числа точно такие же, как натуральные числа, но для каждого натурального числа они имеют дополнительное отрицательное число: -1, -2, -3.

Итак, с учетом сказанного, мы можем реализовать целые числа, используя натуральные числа:

Методы

Я не буду утомлять вас всеми ними, я просто покажу пару, чтобы дать вам
представление:

Как видите, добавление дополнительного элемента в состояние сильно усложнило ситуацию. Несмотря на то, что базовые операции с натуральными числами уже были реализованы, мне все же пришлось добавить к этому много логики. Большие государства - это плохо, дети!

Что дальше?

Давайте проанализируем, что мы уже сделали. Одна из вещей, которые мне показались интересными, - это количество методов внутри наших классов. Это интересный вопрос, есть ли у класса IntegerNumber много обязанностей или нет.

Методов действительно много. Прямо сейчас мы сталкиваемся с проблемой «толстых моделей» из Rails. Что мы можем сделать? Мы можем извлечь поведение в другие классы. Я думаю, что это хороший дизайн, когда данные и поведение разделены. Попробуем немного:

Проблема здесь в том, что нам действительно нужно сделать #value общедоступным. Это интересно, потому что, с одной стороны, мы хотим сделать IntegerNumber просто классом данных, но, с другой стороны, мы не хотим раскрывать его внутреннее состояние, поскольку оно настолько низкоуровневое. Думаю, нам просто нужно пойти на жертвы или разрешить использование send в Add и всех подобных классах.

С другой стороны, мы можем добавить в этот дизайн некоторые функции функционального программирования. Например, каррирование:

Без наших «основных правил» это выглядело бы еще лучше.

Заключение?

Это был интересный опыт, потому что в процессе у меня возникли некоторые мысли о разработке программного обеспечения. Я этим ничего не доказал и ничего нового не открыл. Но я немного повеселился.

Вы можете найти код здесь. Надеюсь, мне понравится еще кое-что, связанное с
парадигмами программирования.

Пока вы здесь: carwow нанимает разработчиков!
Заинтересованы? Ознакомьтесь с нашими доступными ролями
🙇