Могу поспорить, что вы хорошо знакомы с циклами for
while
Когда вы в последний раз использовали его в Ruby? Руби познакомила меня с новым миром циклов, называемых итераторами. Это был первый раз, когда я баловался с each
map
С тех пор мы дружили, и я не оглядывался назад.
Хотя недавно я проводил время, изучая Clojure. Clojure предпочитает объекты значений изменчивым классам, предоставляет богатые неизменяемые структуры данных и подчеркивает функциональное программирование. Поскольку языки идут, это далеко от Ruby. Во время учебы я был удивлен, увидев еще один стиль петли.
Чувствуя вдохновение, я решил перенести этот новый цикл Clojure на Ruby. Я решил использовать продолжения, малоизвестную функцию Ruby, чтобы все это работало. Давайте рассмотрим, как работают эти циклы, что такое продолжения и что происходит, когда сталкиваются эти миры.
О, прежде чем мы начнем, есть только одна вещь. Вам нужно будет пройти ускоренный курс в Clojure. Это займет всего минуту, а остальная часть не будет читаться, как болтовня.
Основы Clojure
Давайте начнем с синтаксиса функции. Важно знать, как работает вызов основной функции. Начнем со знакомого и добавим в Ruby массив целых чисел:
> [1, 2, 3, 4].reduce(:+)
10
В Ruby вы начинаете с массива и вызываете его. Вы говорите reduce
reduce
Это делает работу и дает вам обратно +
Я упоминал ранее, что Clojure имеет функциональную направленность. Это означает, что вы не просто создаете 10
Вместо этого вы передаете все, что используется правильно, чтобы функционировать самому Вот тот же код в Clojure:
[Clojure]
> (уменьшить + [1 2 3 4])
10
[/ Clojure]
Первое, что вы заметите, это то, что круглые скобки окружают все. Внутри них начните с вызываемой функции, которая в данном примере является new
Затем добавьте функцию reduce
С этим знанием вы готовы к обучению +
Петля Clojure
В Clojure loop
Первый аргумент, который вы предоставляете — это массив начальных значений, за которым следует код, вызываемый во время итерации:
[Clojure]
(цикл [х 0]
(печать х))
[/ Clojure]
Сложная часть кода выше — loop
Видите ли, [x 0]
Каждая пара состоит из переменной и начального значения переменной. Этот бит кода устанавливает loop
x
Если бы у нас было более одной переменной, мы добавили бы одну сразу за другой. Это может выглядеть примерно так:
[Clojure]
[х 0
у 1
я 2]
[/ Clojure]
Он также может быть записан как 0
После установки переменных дайте [x 0, y 1, z 2]
Мы оставим это простым и продолжим с loop
(println x)
В этот момент вам может быть интересно, как мы остановим этот цикл. Я открою тебе секрет. Наш код на самом деле не зацикливается вообще. Он запустит одну итерацию и выйдет. Если нам нужна еще одна итерация, мы должны вызвать puts x
[Clojure]
(цикл [х 0]
(печать x)
(повтор (+ x 1)))
[/ Clojure]
Функция recur
recur
loop
Именно так Clojure обрабатывает рекурсивные функции и создает рекурсивные циклы. Нашему function
loop
x
Вы, вероятно, выяснили, что (+ x 1)
(+ x 1)
x + 1
x
Теперь мы создали бесконечный цикл. Я виню тебя.
Clojure работает таким образом, потому что вы не можете переназначать переменные. Мы должны создать новую итерацию цикла с его собственной областью действия, где 2
x
2
2
В контексте Ruby это выглядит очень чуждо. Однако у него есть интересное преимущество: вы можете вызывать 2
recur
Давайте посмотрим, почему это может быть полезно.
Главные факторы
Главными факторами числа являются простые числа, которые можно умножить вместе, чтобы достичь этого числа. Для 15 это означает 3 и 5. В случае 60 основными множителями являются 2, 2, 3 и 5. Обратите внимание, что вам разрешено повторять простое число.
Как мы будем вычислять главные факторы для числа? Начнем с делителя 2, первого простого числа. Посмотрим, делится ли наше число на делитель поровну. Если это так, мы сохраним его в списке простых чисел и попробуем еще раз на нашем новом меньшем числе. Если нет, мы увеличим делитель и попробуем это. Когда мы нажмем 1, мы закончили.
Вот реализация функции prime-factors
[Clojure]
(определить простые факторы [число]
(цикл [оставшееся число
простые числа []
делитель 2]
(конд
(= оставшийся 1) ;; остановиться на 1
простые числа
(= (остаток оставшегося делителя) 0) ;; делится поровну
(рекуррентный (/ оставшийся делитель) (делитель на простые числа) делитель)
: еще ;; не делится поровну
(повторить оставшиеся простые числа (+ делитель 1)))))
[/ Clojure]
Я использовал cond
case
Каждое условие учитывается и сочетается с соответствующим действием. Строки, за которыми нужно следить, — это 9 и 11. В строке 9 я вызываю recur
conj
push
В строке 11, когда делитель не может равномерно разделить число, я увеличиваю делитель и пытаюсь снова.
Реализация оказывается довольно простой. Код читается очень похоже на текст, описывающий, как мы вычисляем простые факторы.
Хорошо, этого достаточно, Clojure.
Давай руби
Как бы мы реализовали loop
recur
Мне нравится начинать с примера того, как я хочу, чтобы это работало.
Давайте напишем ту же функцию простых чисел в 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 уже есть свой loop
Clojure
Я думаю, это выглядит довольно хорошо. Время реализации!
Вниз к делу
Для начала давайте посмотрим, сможем ли мы запустить блок:
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. Начнем с установки переменной mark
nil
Говоря о следующей строке, многое происходит в строке 4.
Продолжения создаются с использованием callcc
Вы заметите, что это требует блока. Он немедленно выполнит блок и передаст ему объект продолжения. В блоке установите mark
Затем верните 1
callcc
К тому времени, когда мы дойдем до строки 5, mark
number
1
Строка 5 говорит сама за себя. В строке 6 находится вторая половина магии.
Использование call
В данном случае это строка 4. Значения, переданные в call
callcc
Когда строка 6 завершена, мы возвращаемся к строке 4, number
2
number
10
Вернемся к нашему коду 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
Если мы будем использовать call
block
Теперь у нас есть код, который может работать бесконечно. Опять я вас виню.
> 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, подготовив переменную для хранения продолжения. Добавьте переменную args
initial_args
Теперь, когда значения передаются в call
args
Еще раз подсчитав от 1 до 10:
Clojure.loop(1) do |number|
puts number
call(number + 1) unless number == 10
end
На данный момент наш loop
Осталось сделать только псевдоним recur
call
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, они получат вашу поддержку.