Статьи

Петли Clojure в рубине

rubyclojure

Могу поспорить, что вы хорошо знакомы с циклами forwhile Когда вы в последний раз использовали его в Ruby? Руби познакомила меня с новым миром циклов, называемых итераторами. Это был первый раз, когда я баловался с eachmap С тех пор мы дружили, и я не оглядывался назад.

Хотя недавно я проводил время, изучая Clojure. Clojure предпочитает объекты значений изменчивым классам, предоставляет богатые неизменяемые структуры данных и подчеркивает функциональное программирование. Поскольку языки идут, это далеко от Ruby. Во время учебы я был удивлен, увидев еще один стиль петли.

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

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

Основы Clojure

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

 > [1, 2, 3, 4].reduce(:+)
10

В Ruby вы начинаете с массива и вызываете его. Вы говорите reducereduce Это делает работу и дает вам обратно +

Я упоминал ранее, что Clojure имеет функциональную направленность. Это означает, что вы не просто создаете 10 Вместо этого вы передаете все, что используется правильно, чтобы функционировать самому Вот тот же код в Clojure:

[Clojure]
> (уменьшить + [1 2 3 4])
10
[/ Clojure]

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

С этим знанием вы готовы к обучению +

Петля Clojure

В Clojure loop Первый аргумент, который вы предоставляете — это массив начальных значений, за которым следует код, вызываемый во время итерации:

[Clojure]
(цикл [х 0]
(печать х))
[/ Clojure]

Сложная часть кода выше — loop Видите ли, [x 0] Каждая пара состоит из переменной и начального значения переменной. Этот бит кода устанавливает loopx Если бы у нас было более одной переменной, мы добавили бы одну сразу за другой. Это может выглядеть примерно так:

[Clojure]
[х 0
у 1
я 2]
[/ Clojure]

Он также может быть записан как 0

После установки переменных дайте [x 0, y 1, z 2] Мы оставим это простым и продолжим с loop(println x)

В этот момент вам может быть интересно, как мы остановим этот цикл. Я открою тебе секрет. Наш код на самом деле не зацикливается вообще. Он запустит одну итерацию и выйдет. Если нам нужна еще одна итерация, мы должны вызвать puts x

[Clojure]
(цикл [х 0]
(печать x)
(повтор (+ x 1)))
[/ Clojure]

Функция recurrecurloop Именно так Clojure обрабатывает рекурсивные функции и создает рекурсивные циклы. Нашему functionloopx Вы, вероятно, выяснили, что (+ x 1)(+ x 1)x + 1x Теперь мы создали бесконечный цикл. Я виню тебя.

Clojure работает таким образом, потому что вы не можете переназначать переменные. Мы должны создать новую итерацию цикла с его собственной областью действия, где 2x22

В контексте Ruby это выглядит очень чуждо. Однако у него есть интересное преимущество: вы можете вызывать 2recur

Давайте посмотрим, почему это может быть полезно.

Главные факторы

Главными факторами числа являются простые числа, которые можно умножить вместе, чтобы достичь этого числа. Для 15 это означает 3 и 5. В случае 60 основными множителями являются 2, 2, 3 и 5. Обратите внимание, что вам разрешено повторять простое число.

Как мы будем вычислять главные факторы для числа? Начнем с делителя 2, первого простого числа. Посмотрим, делится ли наше число на делитель поровну. Если это так, мы сохраним его в списке простых чисел и попробуем еще раз на нашем новом меньшем числе. Если нет, мы увеличим делитель и попробуем это. Когда мы нажмем 1, мы закончили.

Вот реализация функции prime-factors

[Clojure]
(определить простые факторы [число]
(цикл [оставшееся число
простые числа []
делитель 2]
(конд
(= оставшийся 1) ;; остановиться на 1
простые числа
(= (остаток оставшегося делителя) 0) ;; делится поровну
(рекуррентный (/ оставшийся делитель) (делитель на простые числа) делитель)
: еще ;; не делится поровну
(повторить оставшиеся простые числа (+ делитель 1)))))
[/ Clojure]

Я использовал condcase Каждое условие учитывается и сочетается с соответствующим действием. Строки, за которыми нужно следить, — это 9 и 11. В строке 9 я вызываю recurconjpush

В строке 11, когда делитель не может равномерно разделить число, я увеличиваю делитель и пытаюсь снова.

Реализация оказывается довольно простой. Код читается очень похоже на текст, описывающий, как мы вычисляем простые факторы.

Хорошо, этого достаточно, Clojure.

Давай руби

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

Давайте напишем ту же функцию простых чисел в Ruby:

 def prime_factors(number)
  Clojure.loop(number, [], 2) do |remaining, primes, divisor|
    case
    when remaining == 1
      primes
    when remaining % divisor == 0
      recur(remaining / divisor, primes.push(divisor), divisor)
    else
      recur(remaining, primes, divisor + 1)
    end
  end
end

У Ruby уже есть свой loopClojure Я думаю, это выглядит довольно хорошо. Время реализации!

Вниз к делу

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

 class Clojure
  def self.loop(*initial_args, &block)
    block.call(*initial_args)
  end
end

Это даст нам один цикл, как и в версии Clojure.

 > Clojure.loop(1) { |x| puts x }
1

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

Продолжения на помощь! Ruby поставляется с продолжениями как часть стандартной библиотеки. Все, что вам нужно сделать, это require 'continuation'

Если вы прочитаете это и подумали: «А что дальше?», Не волнуйтесь, это нормальный, здоровый ответ. Теперь позвольте мне исказить ваш мозг.

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

Давайте посмотрим на пример, который насчитывает от 1 до 10:

 require 'continuation'

mark = nil
number = callcc { |continuation| mark = continuation; 1 }
puts number
mark.call(number + 1) unless number == 10

Давайте разберем его, начиная со строки 3. Начнем с установки переменной marknil Говоря о следующей строке, многое происходит в строке 4.

Продолжения создаются с использованием callcc Вы заметите, что это требует блока. Он немедленно выполнит блок и передаст ему объект продолжения. В блоке установите mark Затем верните 1callcc

К тому времени, когда мы дойдем до строки 5, marknumber1 Строка 5 говорит сама за себя. В строке 6 находится вторая половина магии.

Использование call В данном случае это строка 4. Значения, переданные в callcallcc
Когда строка 6 завершена, мы возвращаемся к строке 4, number2number10

Вернемся к нашему коду loop Нам нужен способ запустить еще одну итерацию цикла. Использование call Мы создадим продолжение, а затем выполним блок в контексте продолжения:

 require 'continuation'

class Clojure
  def self.loop(*initial_args, &block)
    continuation = nil
    callcc { |c| continuation = c }

    continuation.instance_exec(*initial_args, &block)
  end
end

Если мы будем использовать callblock Теперь у нас есть код, который может работать бесконечно. Опять я вас виню.

 > Clojure.loop(1) { |x| puts x; call }
1
1
...

Мы приближаемся, но все еще не можем передать значения следующей итерации. Давайте это исправим.

 require 'continuation'

class Clojure
  def self.loop(*initial_args, &block)
    continuation = nil
    args = callcc do |c|
      continuation = c

      initial_args
    end

    continuation.instance_exec(*args, &block)
  end
end

Как и в нашем примере подсчета, начните со строки 5, подготовив переменную для хранения продолжения. Добавьте переменную argsinitial_args Теперь, когда значения передаются в callargs

Еще раз подсчитав от 1 до 10:

 Clojure.loop(1) do |number|
  puts number
  call(number + 1) unless number == 10
end

На данный момент наш loop Осталось сделать только псевдоним recurcall

 require 'continuation'

class Clojure
  def self.loop(*initial_args, &block)
    continuation = nil
    args = callcc do |c|
      continuation = c

      class << continuation
        alias :recur :call
      end

      initial_args
    end
    continuation.instance_exec(*args, &block)
  end
end

Мы сделали это!

Единственное, чего мы должны бояться …

Если в какой-то момент во время этого вы подумали «GOTO» и с криком побежали от своего стола к растерянности ваших коллег, это не совсем необоснованно. Как и GOTO, продолжения можно использовать для зла. Если вы небрежно засоряете свой код продолжениями, вы можете ожидать пути выполнения, по которому невозможно следовать. Это также будет означать, что вы делаете это неправильно.

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

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