UPD. посмотрите мой новый похожий пост, но еще круче: https://weird-programming.dev/oop/classes-only.html
Для меня объектно-ориентированное программирование означает, что система разделена на объекты. Объект - это просто сущность, которая имеет некоторое состояние и некоторое поведение. Вы можете заставить свой объект что-то делать, отправив ему сообщение в надежде, что он вас поймет.
Из практических соображений в каждом языке есть некоторые примитивы; основные типы данных, которые вы можете использовать для написания своей программы. Хотя Ruby предположительно является чистым объектно-ориентированным языком (все является объектом), тем не менее в нем есть некоторые примитивы.
Например, числа. Они выглядят как объекты, у них есть методы и прочее. Однако что они на самом деле? 2
и 3
- разные экземпляры класса Integer
, поэтому предполагается, что они имеют разное состояние. Но что это за состояние? Магия.
Давайте попробуем реализовать числа самостоятельно, без магии. Просто для развлечения.
Основные правила
Итак, я придумал этот набор ограничений:
- Мы не можем использовать какие-либо базовые типы, кроме
nil
,true
иfalse
. - Нет
Stdlib
(да). - Блоки хороши (только для выразительности не помешают).
- Можно использовать оператор равенства для объектов. Это необходимо для проверки, указывают ли две указанные ссылки на один и тот же объект.
- Правила не применяются к тестам, потому что тесты нужны только для того, чтобы проверить, работает ли что-то должным образом.
- Правила не применяются к методу
#inspect
, поскольку он служит только для демонстрационных целей.
Правило №1 довольно спорное. С одной стороны, мы используем магические примитивы. С другой стороны, я считаю, что каждая программа должна иметь логические выражения, чтобы иметь какое-либо применение. И у нас не может быть логических выражений без «ложных» сущностей (nil
и false
).
Я считаю, что нам действительно не нужны true
и false
, потому что мы можем использовать nil
для false
и любой объект для true
. Однако почему бы и нет? Просто для выразительности.
Идея реализации
Одна из вещей, которые я помню из своего времени в университете, - это наш профессор, показывающий нам способ реализации натуральных чисел в терминах аксиом Пеано во время лекции по теории множеств.
По сути, нам нужно:
- Некоторая базовая сущность (она будет представлять ноль в наборе натуральных чисел).
- Некоторая функция
next(x)
, которая возвращает число послеx
.
В теории множеств мы можем использовать:
- Пустой набор
[]
- Функция, которая возвращает набор из 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 нанимает разработчиков!
Заинтересованы? Ознакомьтесь с нашими доступными ролями 🙇