Программирование только классами

от автора

image

В моем посте 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   end end

Метод if_branching — основа нашей логической системы. Как видите, мы сразу же предполагаем, что любой объект правдив, так что мы возвращаем then_val.

Что насчет лжи? Давайте начнем с null:

class NullObject < BaseObject   def if_branching(_then_val, else_val)     else_val   end end

То же самое, но возвращаем второй параметр.

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

class NormalObject < BaseObject end

Все, что мы определим позже должно быть унаследовано от 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   end end

И все! Я серьезно. Оно просто работает.

Гляньте вот этот пример:

class Fries < NormalObject end  class Ketchup < NormalObject end  class BurgerMeal < NormalObject   def initialize(fries = NullObject.new)     @fries = fries   end    def sauce     If.new(@fries, Ketchup.new).result   end end  BurgerMeal.new.sauce # ==> NullObject BurgerMeal.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   end end  class OrderTea   def call     # do something else   end end  If.new(today_is_friday?, OrderBeers.new, OrderTea.new)   .result   .call

Собственно, код не будет исполнен, пока мы явно не вызовем метод #call. Вот и все. Таким образом мы можем комбинировать сложную логику и наш класс If.

Булевы типы (просто потому, что мы можем)

У нас уже есть логические типы (null и все остальное), но было бы неплохо добавить специальные булевы классы для выразительности. Приступим:

class Bool < NormalObject; end  class TrueObject < Bool; end  class FalseObject < Bool   def if_branching(_then_val, else_val)     else_val   end end

Мы определили собирательный класс 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)   end end

Оно всего-лишь "переворачивает" аргументы для #if_branching. Просто, но очень полезно.

Циклы

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

В целом он выглядит так:

while some_condition   do_something end

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

Интересная особенность в нашем случае то, что условие должно быть динамичным — оно должно быть в состоянии меняться между шагами цикла. Процедуры спешат на помощь!

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   end end

Программа для примера

Давайте создадим связные списки и функцию, которая считает сколько в списке null-объектов.

Список

Ничего особенного:

class List < NormalObject   def initialize(head, tail = NullObject.new)     @head = head     @tail = tail   end    def head     @head   end    def tail     @tail   end end

Еще нам нужно как-то его обходить (никаких #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   end end

Думаю, основная логика вполне проста. Нам также понадобились вспомогательные процедуры для #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)   end end

У нас пока нет чисел и я решил не тратить время на их создание. Вместо этого я использовал списки (гляньте мой пост про создание чисел здесь).

Интересная штука здесь — метод #inc_callable. Мне кажется, если бы мы хотели разработать наш собственный "язык" со всеми этими базовыми классами, то это могло быть принятым соглашанием добавлять методы, оканчивающиеся на _callable и возвращающие процедуру. Это что-то вроде передачи функций в качестве аргументов в функциональном программировании.

Считаем null в списках

Для начала нам нужна проверка на null. Мы можем добавить ее в NormalObject и NullObject как вспомогательный метод #null? (схожий с #nil? из Ruby):

class NormalObject < BaseObject   def null?     FalseObject.new   end end  class NullObject < BaseObject   def null?     TrueObject.new   end end

Ну а теперь мы можем определить наш 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   end end

Вот и все. Мы можем скормить ему любой список и он подсчитает количество null-объектов в нем.

Заключение

Объектно-ориентированное программирование — очень интересный концепт и, видимо, очень мощный. Мы, по сути, создали язык программирования (!), используя только лишь чистое ООП без каких-либо дополнительных операторов. Все, что нам нужно было: способ создать класс и переменные. Другая прикольная фишка — у нас нет никаких примитивов в нашем языке (например, у нас нет null, вместо этого мы просто создаем экземплярNullObject). О, чудеса программирования…

Код можно найти в моем репозитории experiments.

ссылка на оригинал статьи https://habr.com/ru/post/519660/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *