Есть ли макрос для создания быстрых итераторов из генератор-подобных функций в 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