Есть ли макрос для создания быстрых итераторов из генератор-подобных функций в julia?
Приходя от python3 к Julia, хотелось бы написать итераторы fast как функцию с синтаксисом production/yield или что-то в этом роде.
Макросы Julia, похоже, предполагают, что можно построить макрос, который преобразует такую "генераторную" функцию в итератор julia.
[Кажется, что вы могли бы легко встроить итераторы, написанные в стиле функции, что является особенностью, которую пакет Iterators.jl также пытается обеспечить для своих конкретных итераторов https://github.com/JuliaCollections/Iterators.jl#the-itr-macro-for-automatic-inlining-in-for-loops]
Просто чтобы привести пример того, что я имею в виду:
@asiterator function myiterator(as::Array)
b = 1
for (a1, a2) in zip(as, as[2:end])
try
@produce a1[1] + a2[2] + b
catch exc
end
end
end
for i in myiterator([(1,2), (3,1), 3, 4, (1,1)])
@show i
end
где myiterator
должен идеально создавать быстрый итератор с максимально возможными накладными расходами. И, конечно, это только один конкретный пример. Я идеально хотел бы иметь что-то, что работает со всеми или почти всеми функциями генератора.
Рекомендуемый в настоящее время способ преобразования функции генератора в итератор через Julia Tasks, по крайней мере, насколько мне известно. Однако они также кажутся более медленными, чем чистые итераторы. Например, если вы можете выразить свою функцию с помощью простых итераторов, таких как imap
, chain
и т.д. (Предоставляется пакетом Iterators.jl
) это кажется очень предпочтительным.
Теоретически возможно ли в julia построить макрокоманду, преобразующую функции стиля генератора в гибкие итераторы fast?
Extra-Point-Question: если это возможно, может ли быть общий макрос, который строит такие итераторы?
Ответы
Ответ 1
Генераторы в стиле Python, которые в Julia будут ближе всего к выполнению задач, связаны с достаточным количеством накладных расходов. Вы должны переключать задачи, которые нетривиальны и не могут быть просто устранены компилятором. Именно поэтому итераторы Julia основаны на функциях, которые преобразуют одно типично неизменяемое, простое значение состояния и другое. Короче говоря: нет, я не считаю, что это преобразование можно сделать автоматически.
Ответ 2
Некоторые итераторы этой формы могут быть записаны так:
myiterator(as) = (a1[1] + a2[2] + 1 for (a1, a2) in zip(as, as[2:end]))
Этот код может (потенциально) быть встроенным.
Чтобы полностью обобщить это, теоретически возможно написать макрос, который преобразует свой аргумент в стиль продолжения передачи (CPS), что позволяет приостановить и перезапустить выполнение, предоставив что-то вроде итератора. Для этого особенно подходят ограниченные продолжения (https://en.wikipedia.org/wiki/Delimited_continuation). Результатом является большое гнездо анонимных функций, которое может быть быстрее, чем переключение задач, но не обязательно, так как в конце дня ему нужно присвоить кучу соответствующее количество состояний.
У меня, случается, есть пример такого преобразования (в фемтологизме, хотя и не Джулии): https://github.com/JeffBezanson/femtolisp/blob/master/examples/cps.lsp
Это заканчивается макросом define-generator
, который делает то, что вы описываете. Но я не уверен, что это стоит того, чтобы сделать это для Джулии.
Ответ 3
Подумав много о том, как перевести генераторы python в Julia, не теряя при этом большой производительности, я реализовал и протестировал библиотеку функций более высокого уровня, которые реализуют подобные Python генераторы в стиле продолжения. https://github.com/schlichtanders/Continuables.jl
По сути, идея состоит в том, чтобы рассматривать Python yield
/Julia produce
как функцию, которую мы берем извне в качестве дополнительного параметра. Я назвал его cont
для продолжения. Найдите экземпляр для этой переопределения диапазона
crange(n::Integer) = cont -> begin
for i in 1:n
cont(i)
end
end
Вы можете просто суммировать все целые числа по следующему коду
function sum_continuable(continuable)
a = Ref(0)
continuable() do i
a.x += i
end
a.x
end
# which simplifies with the macro [email protected] to
@Ref function sum_continuable(continuable)
a = Ref(0)
continuable() do i
a += i
end
a
end
sum_continuable(crange(4)) # 10
Как вы, надеюсь, согласитесь, вы можете работать с продолжением почти так же, как если бы вы работали с генераторами в python или задачами в julia. Использование обозначений do
вместо циклов for
- это одна из тех вещей, к которым вы должны привыкнуть.
Эта идея заставляет вас действительно очень далеко. Единственный стандартный метод, который не является чисто реализуемым с использованием этой идеи, - это zip
. Все другие стандартные инструменты более высокого уровня работают так, как вы надеетесь.
Производительность невероятно быстрее, чем задачи и даже быстрее, чем итераторы в некоторых случаях (особенно наивная реализация Continuables.cmap
на порядок быстрее, чем Iterators.imap
). Подробнее читайте в Readme.md репозитория github https://github.com/schlichtanders/Continuables.jl.
EDIT: Чтобы ответить на мой собственный вопрос более прямо, макросу @asiterator
не нужно, просто используйте стиль продолжения напрямую.
mycontinuable(as::Array) = cont -> begin
b = 1
for (a1, a2) in zip(as, as[2:end])
try
cont(a1[1] + a2[2] + b)
catch exc
end
end
end
mycontinuable([(1,2), (3,1), 3, 4, (1,1)]) do i
@show i
end