В моем посте Implementing numbers in "pure" Ruby
("Разрабатываем числа на "чистом" Ruby") я обозначил рамки, которые
разрешали использовал базовые вещи из Ruby вроде оператора
равенства, true
/false
, nil
,
блоки и т.п.
Но что, если бы у нас вообще ничего не было? Даже базовых
операторов вроде if
и while
?
Приготовьтесь к порции чистого объектно-ориентированного
безумия.
Рамки
- Можем определять классы и методы
- Мы должны писать код так, будто в Ruby нет никаких готов
классов из коробки. Просто представьте, что мы начинаем с
абсолютного нуля. Даже
nil
не существует - Единственный оператор, который мы можем использовать
присваивание (
x = something
).
Никакого if-оператора? Серьезно? Он есть даже у процессоров!
Условные операторы важны они являются основой логики для наших программ. Как же справляться без них? Я придумал такое решение: мы можем интегрировать логику во ВСЕ объекты
Сами подумайте, в динамических языках вроде Ruby логические
выражения не обязательно должны вычисляться в какой-нибудь класс
вроде "Boolean". Вместо этого, эти языки считают любой объект
правдимым кроме некоторых особых случаев (nil
и
false
в Ruby;false
, 0
и ''
в JS). Именно
поэтому добавление этого функционала не так уж дико, как кажется на
первый взгляд. Но давайте начнем.
Базовые классы
Давайте создадим самый базовый класс, который будет предком всего остального:
class BaseObject def if_branching(then_val, _else_val) then_val endend
Метод if_branching
основа нашей логической системы.
Как видите, мы сразу же предполагаем, что любой объект правдив, так
что мы возвращаем then_val.
Что насчет лжи? Давайте начнем с null:
class NullObject < BaseObject def if_branching(_then_val, else_val) else_val endend
То же самое, но возвращаем второй параметр.
В Ruby практически все классы наследуются от класса
Object
. Но на самом деле есть другой класс по имени
BasicObject
, который находится даже выше в иерархии.
Давайте сымитируем этот стиль и создадим наш собственный
Object
:
class NormalObject < BaseObjectend
Все, что мы определим позже должно быть унаследовано от
NormalObject
. Потом мы можем добавить в него
глобальные вспомогательные методы (вроде #null?
).
If-выражения
Всего этого уже достаточно для того, чтобы определить наши if-выражения:
class If < NormalObject def initialize(bool, then_val, else_val = NullObject.new) @result = bool.if_branching(then_val, else_val) end def result @result endend
И все! Я серьезно. Оно просто работает.
Гляньте вот этот пример:
class Fries < NormalObjectendclass Ketchup < NormalObjectendclass BurgerMeal < NormalObject def initialize(fries = NullObject.new) @fries = fries end def sauce If.new(@fries, Ketchup.new).result endendBurgerMeal.new.sauce # ==> NullObjectBurgerMeal.new(Fries.new).sauce # ==> Ketchup
Возможно, вы уже думаете: "каким боком нам это полезно, если мы не можем использовать с ним блоки кода?". И что насчет "ленивости"?
Ознакомьтесь с этим примером:
# Псевдокодif today_is_friday? order_beers()else order_tea()end# Наш If классIf.new(today_is_friday?, order_beers(), order_tea()).result
В нашем примере мы закажем пиво ВМЕСТЕ с чаем вне зависимости от дня недели. Это происходит из-за того, что аргументы вычисляются до передачи в конструктор.
И это (ленивость) очень важный механизм, т.к. без него наши программы были бы медленные и даже неправильные.
Решением этого является просто оборачивание кода в другой класс. Позже я буду называть такие обертки процедурами (ориг. "callable"):
class OrderBeers def call # do something endendclass OrderTea def call # do something else endendIf.new(today_is_friday?, OrderBeers.new, OrderTea.new) .result .call
Собственно, код не будет исполнен, пока мы явно не вызовем метод
#call
. Вот и все. Таким образом мы можем комбинировать
сложную логику и наш класс If
.
Булевы типы (просто потому, что мы можем)
У нас уже есть логические типы (null и все остальное), но было бы неплохо добавить специальные булевы классы для выразительности. Приступим:
class Bool < NormalObject; endclass TrueObject < Bool; endclass FalseObject < Bool def if_branching(_then_val, else_val) else_val endend
Мы определили собирательный класс Bool
, класс
TrueObject
без какой либо логики (она не нужна, т.к.
любой экземпляр этого класса уже автоматически будет считаться
правдивым) и классFalseObject
, переопределяющий
#if_branching
так же, как и
NullObject
.
Вот и все. У нас есть специальные булевы классы. Я еще добавил логическое НЕ для удобства:
class BoolNot < Bool def initialize(x) @x = x end def if_branching(then_val, else_val) @x.if_branching(else_val, then_val) endend
Оно всего-лишь "переворачивает" аргументы для
#if_branching
. Просто, но очень полезно.
Циклы
Окей, другая важная вещь в языках программирования циклы. Мы
можем добиться цикличности с помощью рекурсии. Но давайте напишем
специальный оператор While
.
В целом он выглядит так:
while some_condition do_somethingend
Что может быть описано вот так: "если условие выполнено, то сделай вот это и повтори цикл".
Интересная особенность в нашем случае то, что условие должно быть динамичным оно должно быть в состоянии меняться между шагами цикла. Процедуры спешат на помощь!
class While < NormalObject def initialize(callable_condition, callable_body) @cond = callable_condition @body = callable_body end def run is_condition_satisfied = @cond.call If.new(is_condition_satisfied, NextIteration.new(self, @body), DoNothing.new) .result .call end # Запускает "тело" и потом снова While#run. # Таким образом цикличность определена рекурсивно # (жаль, что хвостовая рекурсия не оптимизирована) class NextIteration < NormalObject def initialize(while_obj, body) @while_obj = while_obj @body = body end def call @body.call @while_obj.run end end class DoNothing < NormalObject def call NullObject.new end endend
Программа для примера
Давайте создадим связные списки и функцию, которая считает сколько в списке null-объектов.
Список
Ничего особенного:
class List < NormalObject def initialize(head, tail = NullObject.new) @head = head @tail = tail end def head @head end def tail @tail endend
Еще нам нужно как-то его обходить (никаких #each
с
блоком в этот раз!). Давайте создадим класс, который будет этим
заниматься:
## Позволяет обойти лист один раз#class ListWalk < NormalObject def initialize(list) @left = list end def left @left end # Возвращает текущую голову и присваивает хвост к current. # Возвращает null если конец достигнут def next head = If.new(left, HeadCallable.new(left), ReturnNull.new) .result .call @left = If.new(left, TailCallable.new(left), ReturnNull.new) .result .call head end def finished? BoolNot.new(left) end class HeadCallable < NormalObject def initialize(list) @list = list end def call @list.head end end class TailCallable < NormalObject def initialize(list) @list = list end def call @list.tail end end class ReturnNull < NormalObject def call NullObject.new end endend
Думаю, основная логика вполне проста. Нам также понадобились
вспомогательные процедуры для #head
и
#tail
, чтобы избежать null-pointer ошибок (даже при
том, что наш null на самом деле не null, мы все равно рискуем
вызвать несуществующий метод).
Счетчик
Просто объект, который будет использоваться для подсчетов:
class Counter < NormalObject def initialize @list = NullObject.new end def inc @list = List.new(NullObject.new, @list) end class IncCallable < NormalObject def initialize(counter) @counter = counter end def call @counter.inc end end def inc_callable IncCallable.new(self) endend
У нас пока нет чисел и я решил не тратить время на их создание. Вместо этого я использовал списки (гляньте мой пост про создание чисел здесь).
Интересная штука здесь метод #inc_callable
. Мне
кажется, если бы мы хотели разработать наш собственный "язык" со
всеми этими базовыми классами, то это могло быть принятым
соглашанием добавлять методы, оканчивающиеся на
_callable
и возвращающие процедуру. Это что-то вроде
передачи функций в качестве аргументов в функциональном
программировании.
Считаем null в списках
Для начала нам нужна проверка на null. Мы можем добавить ее в
NormalObject
и NullObject
как
вспомогательный метод #null?
(схожий с
#nil?
из Ruby):
class NormalObject < BaseObject def null? FalseObject.new endendclass NullObject < BaseObject def null? TrueObject.new endend
Ну а теперь мы можем определить наш null-счетчик:
## Возвращает счетчик, увеличенный раз за каждый NullObject в списке#class CountNullsInList < NormalObject def initialize(list) @list = list end def call list_walk = ListWalk.new(@list) counter = Counter.new While.new(ListWalkNotFinished.new(list_walk), LoopBody.new(list_walk, counter)) .run counter end class ListWalkNotFinished < NormalObject def initialize(list_walk) @list_walk = list_walk end def call BoolNot.new(@list_walk.finished?) end end class LoopBody < NormalObject class ReturnNull < NormalObject def call NullObject.new end end def initialize(list_walk, counter) @list_walk = list_walk @counter = counter end def call x = @list_walk.next If.new(x.null?, @counter.inc_callable, ReturnNull.new) .result .call end endend
Вот и все. Мы можем скормить ему любой список и он подсчитает количество null-объектов в нем.
Заключение
Объектно-ориентированное программирование очень интересный
концепт и, видимо, очень мощный. Мы, по сути, создали язык
программирования (!), используя только лишь чистое ООП без
каких-либо дополнительных операторов. Все, что нам нужно было:
способ создать класс и переменные. Другая прикольная фишка у нас
нет никаких примитивов в нашем языке (например, у нас нет
null
, вместо этого мы просто создаем
экземплярNullObject
). О, чудеса
программирования...
Код можно найти в моем репозитории experiments.