Очереди вместо цепочки методов и правил вместо условных выражений в Ruby

Богатый Хикки описывает парадигмы из Clojure и Haskell в своей беседе Простая Made Easy. Будучи программистом по рубинам/рельсам (что я действительно знаю), я любил его идеи, но не понял их 2:

  • Использование очередей, а не цепочка методов
  • Правила вместо условных

Использование очередей вместо

Очевидно, что в Rails мы любим цепочку методов, но я хотел понять, как выглядит Queue в Ruby так, как он описал ее (54:54 в видео):

Если вещь A называет вещь B, вы только что ее составили. У вас есть, когда и где. A должен знать, где B для вызова B. Когда это происходит, всякий раз, когда это происходит, это когда A это делает. Вставьте там очередь.

Правила против условных обозначений

Он говорит о том, что вместо использования условных выражений или операторов switch, кроме правил, вместо этого (30:00 в видео).

Это я просто не понимаю вообще с точки зрения Ruby. Как принимать решения без использования условных обозначений?

Спасибо всем, Джастин

Ответы

Ответ 1

Привет, Очереди

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

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

class Chicken
    def initialize(name)
            @name = name
    end

    def lay_egg
            sleep random(3)
            "an egg from #{@name}"
    end
end

class Farmer
    def initialize(name, chicken)
            @name           = name
            @chicken        = chicken
    end

    def work_shift
            5.times do
                    egg = @chicken.lay_egg
                    puts "#{@name} got #{egg}"
            end
    end
end

betsy       = Chicken.new "Betsy"
fred        = Farmer.new "Fred", betsy
fred.work_shift

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

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

Итак, давайте придерживаться очереди между ними. Курица будет откладывать яйца в верхней части желоба; фермер будет собирать яйца со дна желоба. Ни одна из сторон не полагается напрямую на другую. В коде это может выглядеть так:

class Chicken
    def initialize(name, chute)
            @name   = name
            @chute  = chute
            Thread.new do
                    while true
                            lay_egg
                    end
            end
    end

    def lay_egg
            sleep rand(3)
            @chute << "an egg from #{@name}"
    end
end

class Farmer
    def initialize(name, chute)
            @thread = Thread.new do
                    5.times do
                            egg = chute.pop
                            puts "#{name} got #{egg}"
                    end
            end
    end

    def work_shift
            @thread.join
    end
end

chute       = Queue.new
betsy       = Chicken.new "Betsy", chute
fred        = Farmer.new "Fred", chute
fred.work_shift

Кроме того, теперь мы можем легко добавить вторую курицу. Это вещи, из которых сделаны мечты:

chute       = Queue.new
betsy       = Chicken.new "Betsy", chute
delores     = Chicken.new "Delores", chute
fred        = Farmer.new "Fred", chute
fred.work_shift

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

До свидания, условные обозначения

Мой ответ на это, возможно, немного более спорный, но намного короче. Вы можете взглянуть на multimethods в Ruby, но суть идеи претерпевает закрытые, жестко закодированные логические пути в пользу открытых и в факт, простой полиморфизм достигает именно этого.

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

class Dog
end

class Cat
end

class Bird
end

puts case Bird.new
when Dog then "bark"
when Cat then "meow"
else "Oh no, I didn't plan for this"
end

менее открыто, чем это:

class Dog
    def speak
            "bark"
    end
end

class Cat
    def speak
            "meow"
    end
end

class Bird
    def speak
            "chirp"
    end
end

puts Bird.new.speak

Здесь полиморфизм дал нам возможность описать, как система ведет себя с разными данными, что позволяет нам вводить новое поведение для новых данных по прихоти. Итак, отличная работа, вы (надеюсь) избегаете условностей каждый день!

Ответ 2

Ни одна из этих двух точек не ужасно хорошо воплощена Haskell. Я думаю, что Haskell по-прежнему приводит к некоторому несформированному коду, но подходит ко всей проблеме как с другой философией, так и с различными инструментами.

Очереди

Грубо говоря, Хики хочет указать, что если вы пишете метод на объекте, который вызывает другой объект

class Foo
  def bar(baz)
    baz.quux
  end
end

то мы просто закодировали понятие о том, что все, что передается в Foo#bar, должно иметь метод quux. Это дополнение в его точке зрения, потому что это означает, что реализация Foo по существу связана с реализацией реализации объекта, переданного в Foo#bar.

Это меньше проблема в Ruby, где вызов метода намного больше похож на динамически отправленное сообщение, отправляемое между объектами. Это просто означает, что объект, переданный в Foo#bar, должен как-то ответить ответственно, когда ему присваивается сообщение quux, а не намного больше.

Но это подразумевает последовательность в обработке сообщений. Если вместо этого вы отправили сообщение в очередь, чтобы в конечном итоге быть доставлены к результирующему объекту, тогда вы могли бы легко разместить посредник в этом шве - возможно, вы хотите одновременно запускать bar и quux.

Больше, чем Haskell, эта идея доведена до логического предела в Erlang, и я настоятельно рекомендую узнать, как Erlang решает эти проблемы.

 spawn(fun() -> Baz ! quux)

Правила

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

У Haskell есть сопоставление с образцами, глубоко встроенное в его душу, поэтому трудно утверждать, что Haskell сразу же декомпилирует таким образом... но есть действительно хороший пример системы правил, живущей в разрешении класса Haskell.

Наверное, самым известным из них является mtl -типы, в которых вы заканчиваете писать функции с такими сигнатурами, как

foo :: (MonadReader r m, MonadState s m, MonadIO m, MonadCatch m)
    => a -> m b

где Foo является полностью полиморфным в типе m, если он следует определенным ограничениям - он должен иметь постоянный контекст r, изменяемый контекст s, возможность выполнения IO и способность бросать и ловить исключения.

Фактическое разрешение того, какие типы создают все эти ограничения, решает система правил часто (с любовью или иначе), называемая "пролог типа". Действительно, это достаточно мощная система для кодирования целых программ внутри системы типов.

На самом деле это действительно красиво и дает Haskell своего рода естественный стиль инъекции зависимостей, как описано выше в примере mtl.

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

И что основная проблема с системами правил в целом: вы теряете явный контроль над тем, какие действия заканчиваются стрельбой... поэтому становится сложнее массировать ваши правила, чтобы достичь ожидаемого результата. На самом деле я не уверен, что согласен с Ричем, что системы правил приводят к распаду. Возможно, вы не можете явно выделять информацию, привязанную к другим объектам, но вы настраиваете множество нечетких, долгосрочных зависимостей между вещами.

Ответ 3

Очередь

Использование очередей означает разделение программы на несколько процессов. Например, один процесс, который принимает только электронные письма и помещает их в очередь обработки. Другой процесс вытягивается из очереди обработки и преобразовывает сообщение так или иначе, помещается в "исходящую" очередь. Это позволяет легко заменить некоторые части, не касаясь других. Вы даже можете рассмотреть возможность обработки на другом языке, если производительность плохая. Если вы пишете e=Email.fetch; Processor.process(e), вы вместе выполняете все процессы.

Еще одно преимущество очередей - это может быть много производителей и потребителей. Вы можете легко обрабатывать часть шкалы, просто добавив больше процессов обработки (используя потоки, другие машины и т.д.). С другой стороны, вы можете запускать еще больше процессов "e-mail fetcher". Это сложно, если вы собрали все за один вызов.

В рубине есть простая очередь http://ruby-doc.org/stdlib-1.9.3/libdoc/thread/rdoc/Queue.html и многие другие (rabbitmq, db-based и т.д.)

Правила

Правила делают код несложным. Вместо if-then-else вам предлагается создавать правила. Взгляните на clojure core.match lib:

(use '[clojure.core.match :only (match)])

(doseq [n (range 1 101)]
  (println
    (match [(mod n 3) (mod n 5)]
      [0 0] "FizzBuzz"
      [0 _] "Fizz"
      [_ 0] "Buzz"
      :else n)))

Вы можете написать if (mod3.zero? && mod5.zero?) else if... но будет не так очевидно и (что более важно) трудно добавить больше правил.

Для ruby ​​посмотрите https://github.com/k-tsj/pattern-match, хотя я не использовал такие библиотеки в ruby.

UPDATE:

В своем разговоре Рич упомянул, что пролог-подобная система может использоваться для замены Условий правилами. core.match не настолько мощный, как пролог, но может дать вам представление о том, как упростить условия.