Julia: Альтернативный синтаксис для `map(func, x)`

Созданный на 23 сент. 2014  ·  283Комментарии  ·  Источник: JuliaLang/julia

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

breaking speculative

Самый полезный комментарий

Как только член триумвирата (Стефан, Джефф, Вирал) объединит #15032 (который, я думаю, готов к слиянию), я закрою это и подам дорожную карту, чтобы обрисовать в общих чертах оставшиеся предлагаемые изменения: исправить вычисление типа вещания, объявить устаревшим @vectorize , превратить .op в широковещательный сахар, добавить «широковещательное слияние» на уровне синтаксиса и, наконец, объединить с назначением на месте. Последние два, вероятно, не войдут в 0.5.

Все 283 Комментарий

+1

Или func.(args...) как синтаксический сахар для

broadcast(func, args...)

Но, может быть, я единственный, кто предпочел бы это?
В любом случае +1.

:-1: Во всяком случае, я думаю, что другое предложение Стефана о f[...] имеет хорошее сходство с пониманием.

Как и @ihnorton , мне тоже не очень нравится эта идея. В частности, мне не нравится асимметрия наличия a .+ b и sin.(a) .

Может быть, нам не нужен специальный синтаксис. С #1470 мы могли бы сделать что-то вроде

call(f::Callable,x::AbstractArray) = applicable(f,x) ? apply(f,x) : map(f,x)

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

@quinnj Эта одна строка суммирует мои самые большие опасения по поводу перегрузки вызовов. Я не смогу спать несколько дней.

Пока не уверен, что это синтаксически возможно, но как насчет .sin(x) ? Это больше похоже на a .+ b ?

Я думаю, что [] становится слишком перегруженным и не будет работать для этой цели. Например, мы, вероятно, сможем написать Int(x) , но Int[x] создает массив и поэтому не может означать map .

Я был бы на борту с .sin(x) .

Для этого нам пришлось бы вернуть некоторый синтаксис, но если Int(x) является скалярной версией, то Int[x] разумно по аналогии построить вектор типа элемента Int . На мой взгляд, возможность сделать этот синтаксис более последовательным на самом деле является одним из самых привлекательных аспектов предложения f[v] .

Как создание синтаксиса f[v] для map делает синтаксис более последовательным? Я не понимаю. map имеет другую «форму», чем текущий синтаксис конструктора массива T[...] . А как насчет Vector{Int}[...] ? Разве это не сработает?

лол, извините за испуг @JeffBezanson! Ха-ха, перегрузка вызовов определенно немного пугает, время от времени я думаю о видах запутывания кода, которые вы можете сделать в julia, и с call вы могли бы сделать некоторые грубые вещи.

Я думаю, .sin(x) тоже звучит неплохо. Был ли достигнут консенсус относительно того, что делать с несколькими аргументами?

:-1:. Сохранение пары символов по сравнению с использованием функций более высокого порядка, я не думаю, что стоит затрат на удобочитаемость. Можете ли вы представить себе файл с .func() / func.() и func() , вкрапленными повсюду?

Скорее всего, мы все равно удалим синтаксис a.(b) .

Ух ты, расскажи о развёртывании пчелиного гнезда! Я изменил название, чтобы лучше отражать обсуждение.

Мы также могли бы переименовать map с двумя аргументами в zipWith :)

Если какой-то синтаксис действительно необходим, как насчет [f <- b] или другого каламбура на включение _внутри_ скобок?

( @JeffBezanson , вы просто боитесь, что кто-то собирается написать CJOS или Moose.jl :) ... если мы получим эту функцию, просто поместите ее в раздел руководства Don't do stupid stuff: I won't optimize that )

Текущая запись Int[...] означает, что вы создаете массив элементов типа Int . Но если Int(x) означает преобразование x в Int путем применения Int в качестве функции, то вы также можете считать, что Int[...] означает «применить Int для каждой вещи в ...", что, кстати, приводит к значениям типа Int . Таким образом, запись Int[v] будет эквивалентна [ Int(x) for x in v ] , а Int[ f(x) for x in v ] будет эквивалентна [ Int(f(x)) for x in v ] . Конечно, тогда вы потеряли некоторую полезность написания Int[ f(x) for x in v ] в первую очередь — т. е. что мы можем статически знать, что тип элемента Int — но если применить это Int(x) должно давать значение типа Int (не является необоснованным ограничением), тогда мы сможем восстановить это свойство.

Мне кажется, что больше ада векторизации/неявного кота. Что бы сделал Int[x, y] ? Или, что еще хуже, Vector{Int}[x] ?

Я не говорю, что это лучшая идея, и даже не защищаю ее — я просто указываю, что она _полностью_ не противоречит существующему использованию, которое само по себе является хаком. Если бы мы могли сделать существующее использование частью более согласованного шаблона, это было бы победой. Я не уверен, что означает f[v,w] — очевидный выбор: [ f(x,y) for x in v, y in w ] или map(f,v,w) , но есть и другие варианты.

Я чувствую, что a.(b) почти не используется. Провел быстрый тест, и он используется только в 54 из примерно 4000 исходных файлов julia: https://gist.github.com/jakebolewski/104458397f2e97a3d57d.

Я думаю, что это полностью противоречит. T[x] имеет "форму" T --> Array{T} , а map имеет форму Array{T} --> Array{S} . Это в значительной степени несовместимо.

Чтобы сделать это, я думаю, нам придется отказаться от T[x,y,z] в качестве конструктора для Vector{T} . Обычная старая индексация массива, A[I] , где I — вектор, может рассматриваться как map(i->A[i], I) . «Применение» массива похоже на применение функции (конечно, Matlab даже использует для них тот же синтаксис). В этом смысле синтаксис действительно работает, но в процессе мы потеряли бы синтаксис типизированного вектора.

Я чувствую, что обсуждение синтаксиса здесь отвлекает от более важного изменения: быстрого создания map .

Очевидно, что более важно сделать map быстрым (что, кстати, должно быть частью довольно тщательного изменения понятия функций в julia). Однако переход от sin(x) к map(sin, x) очень важен с точки зрения удобства использования, поэтому, чтобы действительно убить векторизацию, синтаксис очень важен.

Однако переход от sin(x) к map(sin, x) очень важен с точки зрения удобства использования, поэтому, чтобы действительно убить векторизацию, очень важен синтаксис.

Полностью согласен.

Я согласен с @JeffBezanson в том, что f[x] в значительной степени несовместим с текущими конструкциями типизированного массива Int[x, y] и т. д.

Другая причина предпочесть .sin sin. состоит в том, чтобы, наконец, разрешить использовать, например, Base.(+) для доступа к функции + в Base (после a.(b) удаляется).

Когда модуль определяет свою собственную функцию sin (или любую другую), и мы хотим использовать эту функцию для вектора, делаем ли мы Module..sin(v) ? Module.(.sin(v)) ? Module.(.sin)(v) ? .Module.sin(v) ?

Ни один из этих вариантов больше не кажется хорошим.

Я чувствую, что это обсуждение упускает суть проблемы. То есть: при отображении функций с одним аргументом в контейнеры я чувствую, что синтаксис map(func, container) _уже_ понятен и лаконичен. Вместо этого, я чувствую, что только при работе с несколькими аргументами мы могли бы извлечь выгоду из лучшего синтаксиса для каррирования.

Возьмем, к примеру, многословность map(x->func(x,other,args), container) или цепочку операций фильтрации, чтобы сделать ее еще хуже filter(x->func2(x[1]) == val, map(x->func1(x,other,args), container)) .

В этих случаях я чувствую, что укороченный синтаксис карты не сильно поможет. Не то чтобы я думаю, что это особенно плохо, но а) я не думаю, что сокращенная запись map сильно поможет и б) я люблю тосковать по синтаксису Haskell. ;)

IIRC, в Haskell приведенное выше можно записать filter ((==val) . func2 . fst) $ map (func1 other args) container с небольшим изменением порядка аргументов на func1 .

В elm .func определяется как x->x.func , и это очень полезно, см. записи elm . Это следует учитывать, прежде чем использовать этот синтаксис для map .

Мне нравится это.

Хотя доступ к полям не так важен в Джулии, как во многих языках.

Да, здесь это кажется менее актуальным, поскольку поля в Джулии больше предназначены для «частного» использования. Но с продолжающейся дискуссией о перегрузке доступа к полям это может стать более разумным.

f.(x) выглядит менее проблематичным решением, если бы не асимметрия с .+ . Но сохранение символической ассоциации . с «элементарной операцией» - хорошая идея, ИМХО.

Если текущая конструкция типизированного массива может быть признана устаревшей, то func[v...] можно преобразовать в map(func, v...) , а литеральные массивы можно записать в виде T[[a1, ..., an]] (вместо текущего T[a1, ..., an] ).

Я также нахожу sin∘v довольно естественным (когда массив v рассматривается как приложение от индексов к содержащимся значениям), или, проще говоря, sin*v или v*[sin]' ( что требует определения *(x, f::Callable) ) и т. д.

Вернувшись к этому вопросу на свежую голову, я понял, что f.(x) можно рассматривать как вполне естественный синтаксис. Вместо того, чтобы читать его как f. и ( , вы можете читать его как f и .( . Тогда .( метафорически является поэлементной версией оператора вызова функции ( , который полностью совместим с .+ и его аналогами.

Идея того, что .( является оператором вызова функции, меня очень огорчает.

@johnmyleswhite Хотите уточнить? Я говорил об интуитивности синтаксиса или его визуальной согласованности с остальным языком, а вовсе не о технической реализации.

Для меня ( вообще не является частью семантики языка: это просто часть синтаксиса. Так что я бы не хотел изобретать способ для .( и ( начать различаться. Генерирует ли первый multicall Expr вместо call Expr ?

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

Для меня то, что убивает эти варианты, — это вопрос о том, как векторизовать функции с несколькими аргументами. Не существует единого способа сделать это, и все, что является достаточно общим, чтобы поддерживать все возможные способы, начинает очень походить на уже имеющиеся у нас представления многомерных массивов.

Для map с несколькими аргументами вполне стандартно перебирать все аргументы.
Если бы мы сделали это, я бы сделал синтаксис .( для вызова карты. Этот синтаксис может
не так хорошо по разным причинам, но я был бы в порядке с этими аспектами.

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

Нам просто нужно выбрать наиболее полезное решение. Возможные варианты уже обсуждались здесь: https://github.com/JuliaLang/julia/issues/8389#issuecomment -55953120 (и последующие комментарии). Как сказал @JeffBezanson , текущее поведение map разумно. Интересным критерием является возможность замены @vectorize_2arg .

Я хочу сказать, что сосуществование sin.(x) и x .+ y неудобно. Я бы предпочел .sin(x) -> map(sin, x) и x .+ y -> map(+, x, y) .

.+ на самом деле использует broadcast .

Некоторые другие идеи, из чистого отчаяния:

  1. Двоеточие перегрузки, sin:x . Плохо обобщает несколько аргументов.
  2. sin.[x] --- этот синтаксис доступен, в настоящее время не имеет смысла.
  3. sin@x --- не так доступно, но возможно

Я действительно не уверен, что нам это нужно.

И я нет. Я думаю, что f.(x) здесь лучший вариант, но мне он не нравится.

Но без этого как избежать создания всевозможных векторизованных функций, в частности таких, как int() ? Именно это побудило меня начать это обсуждение в https://github.com/JuliaLang/julia/issues/8389.

Мы должны поощрять людей использовать map(func, x) . Это не так уж много, и это сразу понятно любому, кто говорит с другого языка.

И, конечно же, убедитесь, что это быстро.

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

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

Многие другие языки используют map годами.

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

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

И говорить авторам библиотек, чтобы они помещали @vectorize во все подходящие функции, глупо; вы должны иметь возможность просто написать функцию, и если кто-то хочет вычислить ее для каждого элемента, он использует map .

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

  1. Лично я не возражаю против написания map(exp, x) вместо exp(x) , хотя последний немного короче и чище. Однако существует _большая_ разница в производительности. Векторизованная функция примерно в 5 раз быстрее, чем карта на моей машине.
  2. Когда вы работаете с составными выражениями, проблема становится более интересной. Рассмотрим составное выражение: exp(0.5 * abs2(x - y)) , тогда мы имеем
# baseline: the shortest way
exp(0.5 * abs2(x - y))    # ... takes 0.03762 sec (for 10^6 elements)

# using map (very cumbersome for compound expressions)
map(exp, 0.5 * map(abs2, x - y))   # ... takes 0.1304 sec (about 3.5x slower)

# using anonymous function (shorter for compound expressions)
map((u, v) -> 0.5 * exp(abs2(u - v)), x, y)   # ... takes 0.2228 sec (even slower, about 6x baseline)

# using array comprehension (we have to deal with two array arguments)

# method 1:  using zip to combine the arguments (readability not bad)
[0.5 * exp(abs2(u - v)) for (u, v) in zip(x, y)]  # ... takes 0.140 sec, comparable to using map

# method 2:  using index, resulting in a slightly longer statement
[0.5 * exp(abs2(x[i] - y[i])) for i = 1:length(x)]  # ... takes 0.016 sec, 2x faster than baseline 

Если мы собираемся удалить векторизованные математические функции, единственным способом, приемлемым как с точки зрения удобочитаемости, так и с точки зрения производительности, будет понимание массива. Тем не менее, они не так удобны, как векторизованная математика.

-1 для удаления векторизованных версий. На самом деле библиотеки, такие как VML и Yeppp, предлагают гораздо более высокую производительность для векторизованных версий, и нам нужно выяснить, как их использовать.

Есть ли они в базе или нет, это другое обсуждение и более широкое обсуждение, но потребность реальна, и производительность может быть выше, чем у нас есть.

@lindahua и @ViralBShah : некоторые из ваших опасений, похоже, основаны на предположении, что мы избавимся от векторизованных функций, прежде чем внесем улучшения в map , но я не верю, что кто-то предлагал это сделать.

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

Я согласен удалить все векторизованные функции (когда map достаточно быстр) и посмотреть, что получится. Я считаю, что интерес к предоставлению удобного синтаксиса будет еще более заметен в этот момент, и еще будет время добавить его, если окажется, что это так.

Я думаю, что Julia отличается от многих других языков, потому что он делает упор на интерактивное использование (длинные выражения раздражают вводить в этом случае) и математические вычисления (формулы должны быть как можно ближе к математическим выражениям, чтобы сделать код читабельным). Отчасти поэтому Matlab, R и Numpy предлагают векторизованные функции (другая причина, конечно, производительность, проблема, которая может исчезнуть в Julia).

Мое ощущение от обсуждения состоит в том, что значение векторизованной математики занижено. На самом деле, одно из основных преимуществ векторизованной математики заключается в краткости выражения — это гораздо больше, чем просто временная мера для «языков с медленным циклом for».

Сравнивая y = exp(x) и

for i = 1:length(x)
    y[i] = exp(x[i])
end

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

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

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

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

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

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

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

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

Код, который наиболее явно описывает происходящее, — это цикл, но, как говорит @lindahua , он оказывается очень многословным, что имеет свои недостатки, особенно в языке, который также предназначен для интерактивного использования.

Это приводит к компромиссу map , который, как мне кажется, ближе к идеалу, но я все же согласен с @lindahua в том, что он недостаточно лаконичен.

В чем я не соглашусь с @lindahua , так это в том, что векторизованные функции — лучший выбор по причинам, о которых я упоминал ранее. Мои рассуждения приводят к тому, что у Джулии должна быть очень краткая запись для map .

Я нахожу то, как Mathematica делает это со своей сокращенной записью, действительно привлекательным. Сокращенная запись для применения функции к аргументу в Mathematica — @ , поэтому вы должны Apply применить функцию f к вектору как: f @ vector . Соответствующая сокращенная запись для отображения функции — /@ , поэтому вы сопоставляете f с вектором как: f /@ vector . Это имеет несколько привлекательных характеристик. Обе короткие руки лаконичны. Тот факт, что оба используют символ @ , подчеркивает, что существует связь между тем, что они делают, но / на карте по-прежнему делает его визуально различимым, чтобы было ясно, когда вы наносите карту и когда вы нет. Это не значит, что Джулия должна слепо копировать нотацию Mathematica, просто хорошая нотация для отображения невероятно ценна.

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

Все это, конечно, зависит от скорости map и анонимных функций. Сейчас Юля находится в странном положении. Раньше в языках научных вычислений функции были векторизованы, потому что циклы были медленными. Это не проблема. Вместо этого в Julia вы векторизовали функции, потому что map и анонимные функции работают медленно. Итак, мы вернулись к тому, с чего начали, но по другим причинам.

Векторизованные библиотечные функции имеют один недостаток — доступны только те функции, которые явно предоставлены библиотекой. То есть, например, sin(x) работает быстро, когда применяется к вектору, а sin(2*x) внезапно становится намного медленнее, поскольку требует промежуточного массива, который необходимо пройти дважды (сначала запись, затем чтение).

Одним из решений может быть библиотека векторизуемых математических функций. Это будут реализации sin , cos и т. д., которые доступны для встраивания в LLVM. LLVM может затем векторизовать этот цикл и, надеюсь, привести к очень эффективному коду. Yeppp, кажется, имеет правильные ядра циклов, но, похоже, не предоставляет их для встраивания.

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

Объедините это с точкой зрения @eschnett , и вы получите:

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

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

Мне нравится идея @eschnett пометить некоторые функции как _vectorizable_, и компилятор может автоматически отображать векторизуемую функцию в массив, не требуя от пользователей явного определения векторизованной версии. Компилятор также может объединить цепочку векторизуемых функций в плавный цикл.

Вот что я имею в виду, вдохновленный комментариями @eschnett :

# The <strong i="11">@vec</strong> macro tags the function that follows as vectorizable
<strong i="12">@vec</strong> abs2(x::Real) = x * x
<strong i="13">@vec</strong> function exp(x::Real) 
   # ... internal implementation ...
end

exp(2.0)  # simply calls the function

x = rand(100);
exp(x)    # maps exp to x, as exp is tagged as vectorizable

exp(abs2(x))  # maps v -> exp(abs2(v)), as this is applying a chain of vectorizable functions

Компилятор также может повторно векторизовать вычисления (на более низком уровне), определяя возможность использования SIMD.

Конечно, @vec должен быть доступен конечному пользователю, чтобы люди могли объявить свои собственные функции векторизуемыми.

Спасибо, @lindahua : ваше разъяснение очень помогает.

Будет ли @vec отличаться от объявления функции @pure ?

@vec указывает, что функция может быть отображена поэлементно.

Не все чистые функции попадают в эту категорию, например, sum — это чистая функция, и я не думаю, что целесообразно объявлять ее как _vectorizable_.

Нельзя ли повторно извлечь свойство vec sum из тегов pure и associative в + вместе со знаниями о том, как reduce / foldl / foldr работают, когда заданы функции pure и associative ? Очевидно, что все это гипотетически, но если Джулия пойдет ва-банк с трейтами для типов, я мог бы представить себе существенное улучшение современного состояния векторизации, также используя трейты для функций.

Я чувствую, что добавление нового синтаксиса противоположно тому, что мы хотим (после того, как мы просто убрали специальный синтаксис для Any[] и Dict). Весь смысл удаления этих векторизованных функций заключается в уменьшении числа особых случаев (и я не думаю, что синтаксис должен отличаться от семантики функций). Но я согласен, что краткая карта была бы полезна.

Так почему бы не добавить краткий инфиксный оператор map ? Здесь я выберу $ произвольно. Это сделало бы пример @lindahua из

exp(0.5 * abs2(x - y))

к

exp $ (0.5 * abs2 $ (x-y))

Теперь, если бы у нас была поддержка пользовательских инфиксных операторов, подобная Haskell, это было бы изменение только одной строки ($) = map . :)

IMO, другие предложения синтаксиса слишком визуально близки к существующему синтаксису и потребуют дополнительных умственных усилий для анализа при просмотре кода:

  • foo.(x) -- визуально похоже на доступ к членам стандартного типа
  • foo[x] -- обращаюсь ли я к x-му члену массива foo или вызываю карту здесь?
  • .foo(x) - имеет проблемы, как указал @kmsquire

И я чувствую, что решение @vec слишком близко к @vectorize , которого мы пытаемся избежать в первую очередь. Конечно, некоторые аннотации типа #8297 было бы неплохо иметь и могли бы помочь в будущем, более умный компилятор сможет затем распознать эти возможности слияния потоков и соответствующим образом оптимизировать. Но мне не нравится идея форсировать это.

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

(x, y) -> exp(0.5 * abs2(x - y)) $ x, y

Интересно, можно ли позаимствовать идею из классного нового Trait.jl в контексте обозначения векторизуемой функции. Конечно, в этом случае мы смотрим на отдельные _экземпляры_ типа Function , которые можно векторизовать или нет, а не на тип julia, имеющий определенный признак.

Теперь, если бы у нас была только Haskell-подобная поддержка определяемых пользователем инфиксных операторов

6582 #6929 недостаточно?

В этом обсуждении есть смысл векторизовать целые выражения с как можно меньшим количеством временных массивов. Пользователям, которым нужен векторизованный синтаксис, не нужен просто векторизованный exp(x) ; они хотели бы написать такие выражения, как

y =  √π exp(-x^2) * sin(k*x) + im * log(x-1)

и пусть он волшебным образом векторизуется

Не было бы необходимости помечать функции как «векторизуемые». Это скорее свойство того, как функции реализованы и безумно доступны Джулии. Если они реализованы, например, на C, то их необходимо скомпилировать в байт-код LLVM (а не в объектные файлы), чтобы оптимизатор LLVM все еще мог получить к ним доступ. Реализация их в Джулии также сработает.

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

К сожалению, такие реализации будут зависеть от аппаратного обеспечения, т.е. может потребоваться выбор разных алгоритмов или разных реализаций в зависимости от того, какие аппаратные инструкции эффективны. Я делал это в прошлом (https://bitbucket.org/eschnett/vecmathlib/wiki/Home) на C++ и с немного другой целевой аудиторией (операции на основе трафарета, которые векторизуются вручную вместо авто-векторизации). компилятор).

Здесь, в Джулии, все было бы проще, поскольку (а) мы знаем, что компилятором будет LLVM, и (б) мы можем реализовать это в Джулии вместо C++ (макросы и шаблоны).

Есть еще одна вещь, которую следует учитывать: если отказаться от некоторых частей стандарта IEEE, то можно значительно повысить скорость. Многие пользователи знают, что, например, денормализованные числа не важны, или что ввод всегда будет меньше, чем sqrt(max(Double)) и т. д. Вопрос заключается в том, предлагать ли быстрые пути для этих случаев. Я знаю, что меня это очень заинтересует, но другие могут предпочесть точно воспроизводимые результаты.

Позвольте мне создать векторизуемый прототип exp в Джулии. Затем мы можем увидеть, как LLVM делает векторизацию цикла и какие скорости мы получаем.

Не слишком ли страшно использовать круглые скобки вокруг аргумента функции~

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

@eschnett Я не думаю, что разумно связывать API (независимо от того, векторизуемы ли функции или нет) с деталями реализации (как функция компилируется). Это звучит довольно сложно для понимания и поддержания стабильности во времени и в разных архитектурах, и это не будет работать при вызове функций во внешних библиотеках, например, log не будет определяться как векторизуемое, поскольку вызывает функцию из openlibm.

Идея OTOH @johnmyleswhite использовать трейты для передачи математических свойств функции может стать отличным решением. ( Предложение @lindahua — это функция, которую я предложил некоторое время назад, но решение с использованием признаков может быть даже лучше.)

Теперь, если бы у нас была только Haskell-подобная поддержка определяемых пользователем инфиксных операторов

6582 #6929 недостаточно?

Я должен был сказать: ... определяемые пользователем инфиксные операторы _non-unicode_, поскольку я не думаю, что мы хотим требовать от пользователей ввода символов Unicode для доступа к таким основным функциям. Хотя я вижу, что $ на самом деле является одним из добавленных, так что спасибо за это! Вау, так это на самом деле работает в Джулии _today_ (даже если это не "быстро"... пока):

julia> ($) = map
julia> sin $ (0.5 * (abs2 $ (x-y)))

Я не знаю, лучший ли это выбор для map , но использование $ для xor действительно кажется пустой тратой времени. Побитовый xor используется не так часто. map намного важнее.

Приведенная выше точка зрения @jiahao очень хороша: отдельные векторизованные функции, такие как exp , на самом деле являются своего рода хаком для получения векторизованных _выражений_, таких как exp(-x^2) . Синтаксис, который делает что-то вроде @devec , был бы действительно ценным: вы бы получили девекторизованную производительность плюс общность отсутствия необходимости индивидуально идентифицировать функции как векторизованные.

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

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

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

a = split("the quick brown")
b = split("fox deer bear")
c = split("jumped over the lazy")
d = split("dog cat")
e = string(a, " ", b., " ", c, " ", d.) # -> 3x2 Vector{String} of the combinations   
# e[1,1]: """["the","quick", "brown"] fox ["jumped","over","the","lazy"] dog"""

Я не уверен, что .b или b. лучше показать, что вы хотите, чтобы это отображалось. В этом случае мне нравится возвращать многомерный результат 3x2, поскольку он представляет форму пинга map .

Глен

Здесь https://github.com/eschnett/Vecmathlib.jl — репо с образцом
реализация exp , написанная так, чтобы ее можно было оптимизировать с помощью LLVM.
Эта реализация примерно в два раза быстрее стандартной exp
реализация в моей системе. Он (вероятно) еще не достигает скорости Йепппа,
вероятно, потому что LLVM не разворачивает соответствующий цикл SIMD, как
агрессивно, как Yeppp. (Я сравнивал разобранную инструкцию.)

Написать векторизуемую функцию exp непросто. Использование выглядит так:

function kernel_vexp2{T}(ni::Int, nj::Int, x::Array{T,1}, y::Array{T,1})
    for j in 1:nj
        <strong i="16">@simd</strong> for i in 1:ni
            <strong i="17">@inbounds</strong> y[i] += vexp2(x[i])
        end
    end
end

где цикл j и аргументы функции существуют только для
целей бенчмаркинга.

Есть макрос @unroll для Юли?

-Эрик

В воскресенье, 2 ноября 2014 г., в 20:26 Тим Холи-уведомления@github.com написал:

Я согласен, что это не свойство функции, это свойство
использование функции. Дискуссия о применении черт кажется
случае лаять не на то дерево.

Ответьте на это письмо напрямую или просмотрите его на GitHub
https://github.com/JuliaLang/julia/issues/8450#issuecomment-61433026 .

Эрик Шнеттер [email protected]
http://www.perimeterinstitute.ca/personal/eschnetter/

отдельные векторизованные функции, такие как exp , на самом деле являются своего рода хаком для получения векторизованных _выражений_, таких как exp(-x^2)

Основной синтаксис для извлечения целых выражений из скалярной области был бы очень интересным. Векторизация — это всего лишь один пример (где целевой областью являются векторы); другим интересным вариантом использования может быть поднятие в матричную область (# 5840), где семантика совершенно другая. В матричной области также было бы полезно изучить, как может работать диспетчеризация для различных выражений, поскольку в общем случае вам понадобятся алгоритмы Шура-Парлетта и другие более специализированные алгоритмы, если вы хотите что-то более простое, например sqrtm . (И с умным синтаксисом вы можете полностью избавиться от функций *m - expm , logm , sqrtm , ...)

Есть макрос @unroll для Юли?

используя Base.Cartesian
@nexpr 4 d->(y[i+d] = exp(x[i+d])

(См. http://docs.julialang.org/en/latest/devdocs/cartesian/, если у вас есть вопросы.)

@jiahao Обобщение этого на матричные функции звучит как интересная задача, но мои знания об этом близки к нулю. У вас есть идеи о том, как это будет работать? Как это сформулировать с векторизацией? Как синтаксис позволит сделать различие между поэлементным применением exp к вектору/матрице и вычислением экспоненциальной матрицы?

@timholy : Спасибо! Я не думал использовать декартову диаграмму для развертывания.

К сожалению, код, созданный @nexprs (или ручным развертыванием), больше не векторизуется. (Это LLVM 3.3, возможно, LLVM 3.5 будет лучше.)

Re: развертывание, см. также пост @toivoh на julia-users . Возможно, также стоит попробовать #6271.

@nalimilan Я еще не продумал это, но скалярный-> матричный подъем было бы довольно просто реализовать с помощью одной функции matrixfunc (скажем). Один гипотетический синтаксис (полностью выдумывающий что-то) может быть

X = randn(10,10)
c = 0.7
lift(x->exp(c*x^2)*sin(x), X)

что бы тогда

  1. определить исходный и целевой домены подъема из X , имеющие тип Matrix{Float64} и имеющие элементы (параметр типа) Float64 (таким образом, неявно определяя подъем Float64 => Matrix{Float64} ) , тогда
  2. вызовите matrixfunc(x->exp(c*x^2)*sin(x), X) , чтобы вычислить эквивалент expm(c*X^2)*sinm(X) , но избегая умножения матрицы.

В каком-то другом коде X может быть Vector{Int} , и подразумеваемый подъем будет от Int до Vector{Int} , а затем lift(x->exp(c*x^2)*sin(x), X) может тогда позвони map(x->exp(c*x^2)*sin(x), X) .

Можно представить и другие методы, явно указывающие исходный и целевой домены, например, lift(Number=>Matrix, x->exp(c*x^2)*sin(x), X) .

@nalimilan Векторизация на самом деле не является свойством API. С современной технологией компилятора функция может быть векторизована только в том случае, если она является встроенной. В основном все зависит от реализации функции - если она написана "правильным образом", то компилятор может ее векторизовать (после встраивания в окружающий цикл).

@eschnett : Вы используете то же значение векторизации, что и другие? Похоже, вы говорите о SIMD и т. д., но я не понимаю, что означает @nalimilan .

Верно. Здесь есть два разных понятия векторизации. Одна сделка
с получением SIMD для узких внутренних циклов (процессорная векторизация). Главный
обсуждаемая здесь проблема - это синтаксис/семантика как-то "автоматически"
возможность вызывать функцию с одним (или несколькими) аргументами для коллекции.

Во вторник, 4 ноября 2014 г., в 19:04, Джон Майлз Уайт, [email protected]
написал:

@eschnett https://github.com/eschnett : вы используете то же значение?
векторизации, как другие? Похоже, вы про SIMD и т. д., что
это не то, что я понимаю @nalimilan https://github.com/nalimilan to
иметь в виду.


Ответьте на это письмо напрямую или просмотрите его на GitHub
https://github.com/JuliaLang/julia/issues/8450#issuecomment-61738237 .

В симметрии с другими операторами . не следует ли f.(x) применять набор функций к набору значений? (Например, для перехода от какой-либо системы координат n-й единицы к физическим координатам.)

При обсуждении синтаксиса возникло мнение, что использование явных циклов для выражения эквивалента map(log, x) слишком медленно. Следовательно, если это можно сделать достаточно быстро, то вызов map (или использование специального синтаксиса) или запись циклов эквивалентны на семантическом уровне, и нет необходимости вводить синтаксическое устранение неоднозначности. В настоящее время вызов векторно-логарифмической функции выполняется намного быстрее, чем запись цикла над массивом, что побуждает людей искать способ выразить это различие в своем коде.

Здесь есть два уровня проблем: (1) синтаксис и семантика, (2) реализация.

Проблема синтаксиса и семантики заключается в том, как пользователь может выразить намерение сопоставления определенных вычислений поэлементно/широковещательно с заданными массивами. В настоящее время Julia поддерживает два способа: использование векторизованных функций и предоставление пользователям возможности писать цикл явно (иногда с помощью макросов). Ни один из способов не идеален. Хотя векторизованные функции позволяют писать очень краткие выражения, такие как exp(0.5 * (x - y).^2) , у них есть две проблемы: (1) трудно провести линию относительно того, какие функции должны предоставлять векторизованную версию, а какие нет, что часто приводит к в бесконечных дебатах со стороны разработчиков и путанице со стороны пользователя (вам часто приходится искать документ, чтобы выяснить, векторизованы ли определенные функции). (2) Это затрудняет объединение циклов через границы функций. На данный момент и, вероятно, через несколько месяцев/лет компилятор, вероятно, не сможет выполнять такие сложные задачи, как просмотр нескольких функций вместе, идентификация объединенного потока данных и создание оптимизированного пути кода через границы функций.

Использование функции map решает проблему (1) здесь. Это, однако, по-прежнему не помогает в решении проблемы (2) - использование функций, будь то конкретная векторизованная функция или общая map , всегда создает границу функции, которая препятствует объединению циклов, что имеет решающее значение для высокопроизводительных вычислений. Использование функции карты также приводит к многословию, например, приведенное выше выражение теперь становится более длинным оператором как map(exp, 0.5 * map(abs2, x - y)) . Вы можете разумно предположить, что эта проблема будет усугубляться более сложными выражениями.

Среди всех предложений, изложенных в этой ветке, я лично считаю, что использование специальных обозначений для обозначения отображения является наиболее многообещающим путем в будущем. Во-первых, он сохраняет лаконичность выражения. Возьмем, к примеру, нотацию $, приведенные выше выражения теперь можно записать как exp $(0.5 * abs2$(x - y)) . Это немного длиннее, чем исходное векторизованное выражение, но это не так уж плохо — все, что требуется, — это вставлять $ в каждый вызов сопоставления. С другой стороны, это обозначение также служит однозначным индикатором выполняемого отображения, которое компилятор может использовать для нарушения границы функции и создания плавного цикла. В этом курсе компилятору не нужно смотреть на внутреннюю реализацию функции — все, что ему нужно знать, это то, что функция будет отображена на каждый элемент данных массивов.

Учитывая все возможности современных ЦП, особенно возможности SIMD, объединение нескольких циклов в один — это только один шаг к высокопроизводительным вычислениям. Этот шаг сам по себе не запускает использование SIMD-инструкций. Хорошая новость заключается в том, что теперь у нас есть макрос @simd . Компилятор может вставить этот макрос в начало созданного цикла, если он считает, что это безопасно и полезно.

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

Резюме @lindahua хорошее, ИМХО.

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

  • Паттерн слияния вложенных вызовов функций в единый цикл следует применять и к операторам, чтобы A .* B .+ C не приводило к созданию двух временных, а только одного для результата.
  • Комбинация поэлементных функций и сокращений также должна обрабатываться, чтобы сокращение применялось на лету после вычисления значения каждого элемента. Как правило, это позволяет избавиться от sumabs2(A) , заменив его стандартным обозначением вроде sum(abs$(A)$^2) (или sum(abs.(A).^2) ).
  • Наконец, нестандартные шаблоны итерации должны поддерживаться для нестандартных массивов, так что для разреженных матриц A .* B нужно обрабатывать только ненулевые элементы и возвращать разреженную матрицу. Это также может быть полезно, если вы хотите применить поэлементную функцию к Set , Dict или даже к Range .

Два последних пункта могут работать, заставляя поэлементные функции возвращать специальный тип AbstractArray , скажем, LazyArray , который будет вычислять свои элементы на лету (аналогично Transpose ). введите с https://github.com/JuliaLang/julia/issues/4774#issuecomment-59422003). Но вместо наивного доступа к его элементам с помощью линейных индексов от 1 до length(A) можно использовать протокол итератора. Итератор для данного типа будет автоматически выбирать, будет ли итерация по строкам или по столбцам наиболее эффективной, в зависимости от схемы хранения типа. А для разреженных матриц это позволило бы пропускать нулевые записи (требуется, чтобы оригинал и результат имели общую структуру, ср. https://github.com/JuliaLang/julia/issues/7010, https://github. com/JuliaLang/julia/issues/7157).

Когда сокращение не применяется, объект того же типа и формы, что и исходный, будет просто заполнен путем итерации по LazyArray (эквивалентно collect , но с учетом типа исходного массива ). Единственное, что для этого нужно, это то, что итератор возвращает объект, который можно использовать для вызова getindex на LazyArray и setindex! на результат (например, линейный или декартов). координаты).

Когда применяется сокращение, он будет использовать соответствующий метод итерации для своего аргумента, чтобы выполнить итерацию по требуемым размерам LazyArray и заполнить массив результатом (эквивалентным reduce , но с использованием пользовательский итератор для адаптации к типу массива). Одна функция (та, что использовалась в последнем абзаце) вернет итератор, перебирающий все элементы наиболее эффективным способом; другие позволяют делать это в определенных измерениях.

Вся эта система также будет поддерживать операции на месте довольно просто.

Я немного подумал о синтаксисе и подумал о .= для применения поэлементных операций к массиву.
Таким образом, пример @nalimilan sum(abs.(A).^2)) , к сожалению, должен быть написан в два этапа:

A = [1,2,3,4]
a .= abs(A)^2
result = sum(a)

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

Конечно, ничто, кроме производительности и привычности, не останавливает любого от простого написания map((x) -> abs(x)^2, A) прямо сейчас, как было сказано.

В качестве альтернативы может работать окружение выражения, которое должно быть отображено, с помощью .() .
Я не знаю, насколько сложно это было бы сделать, но .sin(x) и .(x + sin(x)) затем отображали бы выражение либо в скобках, либо на функцию, следующую за . .
Затем это позволило бы сократить, как в примере @nalimilan , где sum(.(abs(A)^2)) можно было бы записать в одну строку.

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

Я экспериментировал с идеей LazyArray , которую изложил в своем последнем комментарии: https://gist.github.com/nalimilan/e737bc8b3b10288abdad .

В этом доказательстве концепции нет синтаксического сахара, но (a ./ 2).^2 будет переведено в то, что написано в сути как LazyArray(LazyArray(a, /, (2,)), ^, (2,)) . Система работает достаточно хорошо, но ей требуется дополнительная оптимизация, чтобы иметь отдаленную конкуренцию с циклами в отношении производительности. Проблема (ожидаемая) заключается в том, что вызов функции в строке 12 не оптимизирован (почти все выделения происходят там), даже в версии, где дополнительные аргументы не разрешены. Я думаю, что мне нужно параметризовать LazyArray в вызываемой функции, но я не понял, как это сделать, не говоря уже об обработке аргументов. Есть идеи?

Любые предложения о том, как улучшить производительность LazyArray ?

@nalimilan Год назад я экспериментировал с аналогичным подходом, используя типы функторов в NumericFuns для параметризации типов ленивых выражений. Я испробовал множество трюков, но мне не удалось преодолеть разрыв в производительности.

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

@lindahua @inline не имеет никакого значения для таймингов, что для меня логично, поскольку getindex(::LazyArray, ...) специализируется на заданной подписи LazyArray , которая не указывает, какая функция должна называться. Мне нужно что-то вроде LazyArray{T1, N, T2, F} с F функцией, которую следует вызывать, чтобы при компиляции getindex вызов был известен. Есть ли способ сделать это?

Встраивание было бы еще одним большим улучшением, но на данный момент тайминги намного хуже, чем даже у не встроенного вызова.

Вы можете рассмотреть возможность использования NumericFuns , а F может быть типом функтора.

Дахуа

Мне нужны функции, в которых я знаю тип возвращаемого значения для распределенных
вычисления, где я создаю ссылки на результат перед результатом (и
таким образом, его тип) известен. Я сам реализовал очень похожую вещь, и
вероятно, следует переключиться на использование того, что вы называете «функторами». (мне не нравится
имя «функтор», так как обычно они представляют собой что-то другое <
http://en.wikipedia.org/wiki/Functor>, но я думаю, что C++ запутал воду
здесь.)

Я думаю, что было бы разумно отделить часть Functor от
математические функции.

-Эрик

В четверг, 20 ноября 2014 г., в 10:35, Dahua [email protected]
написал:

Вы можете рассмотреть возможность использования NumericFuns, а F может быть типом функтора.

Ответьте на это письмо напрямую или просмотрите его на GitHub
https://github.com/JuliaLang/julia/issues/8450#issuecomment-63826019 .

Эрик Шнеттер [email protected]
http://www.perimeterinstitute.ca/personal/eschnetter/

@lindahua Я пытался использовать функторы, и действительно, производительность намного разумнее:
https://gist.github.com/nalimilan/d345e1c080984ed4c89a

With functions:
# elapsed time: 3.235718017 seconds (1192272000 bytes allocated, 32.20% gc time)

With functors:
# elapsed time: 0.220926698 seconds (80406656 bytes allocated, 26.89% gc time)

Loop:
# elapsed time: 0.07613788 seconds (80187556 bytes allocated, 45.31% gc time) 

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

На самом деле, тест выше использовал Pow , что, по-видимому, дает большую разницу в скорости в зависимости от того, пишете ли вы явный цикл или используете LazyArray . Я предполагаю, что это связано со слиянием инструкций, которые будут выполняться только в последнем случае. То же явление наблюдается, например, при добавлении. Но с другими функциями разница намного меньше, как с матрицей 100x100, так и с матрицей 1000x1000, вероятно, потому, что они внешние и поэтому инлайнинг не сильно выигрывает:

# With sqrt()
julia> test_lazy!(newa, a);
julia> <strong i="8">@time</strong> for i in 1:1000 test_lazy!(newa, a) end
elapsed time: 0.151761874 seconds (232000 bytes allocated)

julia> test_loop_dense!(newa, a);
julia> <strong i="9">@time</strong> for i in 1:1000 test_loop_dense!(newa, a) end
elapsed time: 0.121304952 seconds (0 bytes allocated)

# With exp()
julia> test_lazy!(newa, a);
julia> <strong i="10">@time</strong> for i in 1:1000 test_lazy!(newa, a) end
elapsed time: 0.289050295 seconds (232000 bytes allocated)

julia> test_loop_dense!(newa, a);
julia> <strong i="11">@time</strong> for i in 1:1000 test_loop_dense!(newa, a) end
elapsed time: 0.191016958 seconds (0 bytes allocated)

Поэтому я хотел бы выяснить, почему оптимизации не происходят с LazyArray . Сгенерированная сборка достаточно длинная для простых операций. Например, для x/2 + 3 :

julia> a1 = LazyArray(a, Divide(), (2.0,));

julia> a2 = LazyArray(a1,  Add(), (3.0,));

julia> <strong i="17">@code_native</strong> a2[1]
    .text
Filename: none
Source line: 1
    push    RBP
    mov RBP, RSP
Source line: 1
    mov RAX, QWORD PTR [RDI + 8]
    mov RCX, QWORD PTR [RAX + 8]
    lea RDX, QWORD PTR [RSI - 1]
    cmp RDX, QWORD PTR [RCX + 16]
    jae L64
    mov RCX, QWORD PTR [RCX + 8]
    movsd   XMM0, QWORD PTR [RCX + 8*RSI - 8]
    mov RAX, QWORD PTR [RAX + 24]
    mov RAX, QWORD PTR [RAX + 16]
    divsd   XMM0, QWORD PTR [RAX + 8]
    mov RAX, QWORD PTR [RDI + 24]
    mov RAX, QWORD PTR [RAX + 16]
    addsd   XMM0, QWORD PTR [RAX + 8]
    pop RBP
    ret
L64:    movabs  RAX, jl_bounds_exception
    mov RDI, QWORD PTR [RAX]
    movabs  RAX, jl_throw_with_superfluous_argument
    mov ESI, 1
    call    RAX

В отличие от эквивалента:

julia> fun(x) = x/2.0 + 3.0
fun (generic function with 1 method)

julia> <strong i="21">@code_native</strong> fun(a1[1])
    .text
Filename: none
Source line: 1
    push    RBP
    mov RBP, RSP
    movabs  RAX, 139856006157040
Source line: 1
    mulsd   XMM0, QWORD PTR [RAX]
    movabs  RAX, 139856006157048
    addsd   XMM0, QWORD PTR [RAX]
    pop RBP
    ret

Часть до jae L64 — это проверка границ массива. Использование @inbounds может помочь (при необходимости).

Часть ниже, где две последовательные строки начинаются с mov RAX, ... , представляет собой двойную косвенность, то есть доступ к указателю на указатель (или массив массивов, или указатель на массив и т. д.). Это может быть связано с внутренним представлением LazyArray - возможно, здесь может помочь использование неизменяемых объектов (или представление неизменяемых объектов Джулией по-другому).

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

Также: что произойдет, если вы дизассемблируете это не из REPL, а из функции?

Также нельзя не заметить, что первая версия выполняет настоящую
деление, а второе превратило x/2 в умножение.

Спасибо за комментарии.

@eschnett LazyArray уже неизменяем, и я использую @inbounds в циклах. После запуска gist на https://gist.github.com/nalimilan/d345e1c080984ed4c89a вы можете проверить, что это дает в цикле:

function test_lazy!(newa, a)
    a1 = LazyArray(a, Divide(), (2.0,))
    a2 = LazyArray(a1, Add(), (3.0,))
    collect!(newa, a2)
    newa
end
<strong i="11">@code_native</strong> test_lazy!(newa, a); 

Так что, может быть, все, что мне нужно, это иметь возможность форсировать встраивание? В моих попытках добавление @inline к getindex не меняет тайминги.

@toivoh Чем объяснить, что в последнем случае деление не упрощается?

Я продолжил экспериментировать с версией с двумя аргументами (называемой LazyArray2 ). Оказывается, для простой операции, такой как x .+ y , на самом деле быстрее использовать LazyArray2 , чем текущую .+ , и это также довольно близко к явным циклам (это для 1000 вызовов , см. https://gist.github.com/nalimilan/d345e1c080984ed4c89a):

# With LazyArray2, filling existing array
elapsed time: 0.028212517 seconds (56000 bytes allocated)

# With explicit loop, filling existing array
elapsed time: 0.013500379 seconds (0 bytes allocated)

# With LazyArray2, allocating a new array before filling it
elapsed time: 0.098324278 seconds (80104000 bytes allocated, 74.16% gc time)

# Using .+ (thus allocating a new array)
elapsed time: 0.078337337 seconds (80712000 bytes allocated, 52.46% gc time)

Таким образом, похоже, что эта стратегия жизнеспособна для замены всех поэлементных операций, включая операторы .+ , .* и т. д.

Также выглядит очень конкурентоспособным выполнение общих операций, таких как вычисление суммы квадратов разностей по измерению матрицы, то есть sum((x .- y).^2, 1) (см. снова суть):

# With LazyArray2 and LazyArray (no array allocated except the result)
elapsed time: 0.022895754 seconds (1272000 bytes allocated)

# With explicit loop (no array allocated except the result)
elapsed time: 0.020376307 seconds (896000 bytes allocated)

# With element-wise operators (temporary copies allocated)
elapsed time: 0.331359085 seconds (160872000 bytes allocated, 50.20% gc time)

@налимилан
Ваш подход к LazyArrays похож на то, как паровой синтез работает в Haskell [1, 2]. Может быть, мы сможем применить идеи из этой области?

[1] http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.104.7401
[2] http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.421.8551

@vchuravy Спасибо. Это действительно похоже, но сложнее, потому что Джулия использует императивную модель. Напротив, в Haskell компилятор должен обрабатывать большое количество случаев и даже обрабатывать проблемы SIMD (которые обрабатываются LLVM на более позднем этапе в Julia). Но, честно говоря, я не в состоянии разобрать все в этих бумагах.

@nalimilan Я знаю это чувство. Я нашел вторую статью особенно интересной, поскольку в ней обсуждается Generalized Stream Fusion, которая, по-видимому, позволяет использовать хорошую модель вычислений над векторами.

Я думаю, главное, что мы должны извлечь из этого, это то, что конструкции типа map и reduce в сочетании с ленивостью могут быть достаточно быстрыми (или даже быстрее, чем явные циклы).

Насколько я могу судить, фигурные скобки все еще доступны в синтаксисе вызовов. Что, если это станет func{x} ? Может быть, слишком расточительно?

Что касается быстрой векторизации (в смысле SIMD), можем ли мы каким-либо образом подражать тому, как это делает Эйген?

Вот предложение заменить все текущие поэлементные операции обобщением того, что я назвал LazyArray и LazyArray2 выше. Это, конечно, основано на предположении, что мы можем сделать это быстро для всех функций, не полагаясь на функторы из NumericFuns.jl.

1) Добавьте новый синтаксис f.(x) или f$(x) или любой другой, который создаст вызов $ LazyArray f() для каждого элемента x .

2) Обобщите этот синтаксис, следуя тому, как broadcast работает в настоящее время, так что, например, f.(x, y, ...) или f$(x, y, ...) создает LazyArray , но расширяет одноэлементные размеры x , y , ... чтобы придать им общий размер. Это, конечно, будет сделано на лету путем вычислений по индексам, так что расширенные массивы фактически не будут выделены.

3) Сделайте .+ , .- , .* , ./ , .^ и т. д. используйте LazyArray вместо broadcast .

4) Ввести новый оператор присваивания .= или $= , который преобразует (вызывая collect ) массив LazyArray в реальный массив (типа, зависящего от его входы с помощью правил продвижения и типа элемента в зависимости от типа элемента входов и вызываемой функции).

5) Может быть, даже заменить broadcast вызовом LazyArray и немедленным collect результатов в реальный массив.

Пункт 4 — ключевой: поэлементные операции никогда не возвращали бы настоящие массивы, всегда LazyArray s, так что при объединении нескольких операций не делается копий, а циклы можно объединять для эффективности. Это позволяет вызывать сокращения типа sum для результата без выделения временных объектов. Таким образом, выражения такого рода будут идиоматическими и эффективными как для плотных массивов, так и для разреженных матриц:

y .= sqrt.(x .+ 2)
y .=  √π exp.(-x .^ 2) .* sin.(k .* x) .+ im * log.(x .- 1)
sum((x .- y).^2, 1)

Я думаю, что возврат такого легковесного объекта идеально вписывается в новую картину представления массивов и Transpose / CTranspose . Это означает, что в Julia вы можете очень эффективно выполнять сложные операции с плотным и читаемым синтаксисом, хотя в некоторых случаях вам нужно явно вызывать copy , когда вам нужно сделать «псевдомассив» независимым от реальный массив, на котором он основан.

Это действительно звучит как важная функция. Текущее поведение поэлементных операторов является ловушкой для новых пользователей, так как синтаксис приятный и короткий, но производительность обычно ужасно плохая, явно хуже, чем в Matlab. Буквально на прошлой неделе у нескольких тредов на julia-users были проблемы с производительностью, которые исчезли бы с таким дизайном:
https://groups.google.com/d/msg/julia-users/t0KvvESb9fA/6_ZAp2ujLpMJ
https://groups.google.com/d/msg/julia-users/DL8ZsK6vLjw/w19Zf1lVmHMJ
https://groups.google.com/d/msg/julia-users/YGmDUZGOGgo/LmsorgEfXHgJ

Для целей этой проблемы я бы отделил синтаксис от лени. Хотя ваше предложение интересно.

Кажется, наступил момент, когда осталось _так много точек_. В частности, средний пример лучше было бы записать как

x .|> x->exp(-x ^ 2) * sin(k * x) + im * log(x - 1)

для которого требуются только базовые функции и эффективный map ( .|> ).

Вот интересное сравнение:

y .=  √π exp.(-x .^ 2) .* sin.(k .* x) .+ im * log.(x .- 1)
y =  [√π exp(-x[i]^ 2) .* sin(k * x[i]) .+ im * log(x[i] - 1) for i = 1:length(x)]

Если не принимать во внимание часть for ... , понимание становится только на один символ длиннее. Я бы предпочел сокращенный синтаксис понимания, чем все эти точки.

Одномерное понимание не сохраняет форму, но теперь, когда у нас есть for i in eachindex(x) , это тоже может измениться.

Одна проблема с пониманиями заключается в том, что они не поддерживают DataArrays.

Я думаю, что, возможно, стоит взглянуть на целую кучу вещей, которые произошли в .Net и которые очень похожи на идею LazyArray. По сути, для меня это выглядит довольно близко к подходу в стиле LINQ, где у вас есть синтаксис, который выглядит как поэлементный материал, который у нас есть прямо сейчас, но на самом деле этот синтаксис создает дерево выражений, и это дерево выражений затем оценивается некоторым эффективным способом. . Это как-то близко?

В .Net далеко зашли с этой идеей: вы можете выполнять эти деревья выражений параллельно на нескольких процессорах (добавляя .AsParallel()), или вы можете запускать их на большом кластере с DryadLINQ или даже на http:/ /research.microsoft.com/en-us/projects/accelerator/ (последний, возможно, не был полностью интегрирован с LINQ, но близок по духу, если я правильно его помню), или, конечно, он мог быть переведен в SQL, если это данные были в такой форме, и вы использовали только операторы, которые можно было преобразовать в операторы SQL.

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

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

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

Можно представить себе адаптацию синтаксиса понимания, что-то вроде y = [sqrt(x + 2) over x] . Но, как отметил @johnmyleswhite , они должны поддерживать DataArrays , а также разреженные матрицы и любой новый тип массива. Итак, это снова случай смешения синтаксиса и функций.

Если говорить более фундаментально, я думаю, что мое предложение предлагает две особенности по сравнению с альтернативами:
1) Поддержка назначения на месте без распределения с использованием y[:] = sqrt.(x .+ 2) .
2) Поддержка сокращений без распределения, таких как sum((x .- y).^2, 1) .

Может ли это быть обеспечено другими решениями (без учета проблем синтаксиса)?

@davidanthoff Спасибо, смотрю на это сейчас (я думаю, что LazyArray можно было бы сделать и для поддержки параллельных вычислений).

Возможно, это можно было бы совместить с Генераторами — они тоже своего рода ленивый массив. Мне несколько нравится синтаксис понимания [f(x) over x] , хотя он может быть концептуально сложным для новичков (поскольку одно и то же имя фактически используется как для элементов, так и для самого массива). Если бы включения без скобок создавали генератор (как я играл с ним давным-давно ), то было бы естественно использовать эти новые включения в стиле over-x без скобок для возврата LazyArray вместо того, чтобы сразу собирать его.

@mbauman Да, генераторы и ленивые массивы имеют много общих свойств. Идея использовать скобки для сбора генератора/ленивого массива и не добавлять их, чтобы сохранить ленивый объект, звучит круто. Итак, что касается моих примеров выше, можно было бы написать как 1) y[:] = sqrt(x + 2) over x , так и sum((x - y)^2 over (x, y), 1) (хотя я нахожу это естественным даже для новичков, давайте оставим вопрос о over для сессию байкшеринга и сначала сосредоточьтесь на основах).

Мне нравится идея f(x) over x . Мы могли бы даже использовать f(x) for x , чтобы избежать нового ключевого слова. На самом деле [f(x) for x=x] уже работает. Затем нам нужно сделать включения эквивалентными map , чтобы они могли работать для не-массивов. Array будет просто значением по умолчанию.

Должен признаться, мне тоже понравилась идея over . Одно различие между over в качестве карты и for в понимании списка заключается в том, что происходит в случае нескольких итераторов: [f(x, y) for x=x, y=y] приводит к матрице. Для случая с картой вам обычно нужен вектор, т. Е. [f(x, y) over x, y] будет эквивалентно [f(x, y) for (x,y) = zip(x, y)]] . Из-за этого я все еще думаю, что введение дополнительного ключевого слова over того стоит, потому что, поскольку эта проблема поднялась, map обход нескольких векторов очень распространен и должен быть кратким.

Эй, я убедил Джеффа в синтаксисе! ;-)

Это относится к # 4470, поэтому пока добавляйте к 0.4-проектам.

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

  • работает с различными типами данных, такими как DataArrays, а не только с нативными массивами;
  • так же быстро, как вручную написанный цикл.

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

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

Это могло бы заставить создателей структур данных писать такие макросы, но это было бы необходимо только в том случае, если бы их автор хотел, чтобы они имели действительно эффективное выполнение.

Если то, что я пишу, неясно, я имею в виду, что что-то вроде [EXPR for i in collection if COND] может быть переведено в eval(collection_mapfilter_macro(:(i), :(EXPR), :(COND))) , где collection_mapfilter_macro выбирается на основе предполагаемого типа коллекции.

Нет, мы не хотим делать такие вещи. Если DataArray определяет map (или эквивалент), его определение всегда должно вызываться для DataArrays независимо от того, что можно вывести.

На самом деле проблема не в реализации, а в синтаксисе. Прямо сейчас многие люди привыкли к неявному отображению sin(x) , если x является массивом, но с этим подходом возникает много проблем. Вопрос в том, какой альтернативный синтаксис был бы приемлем.

1) Поддержка назначения на месте без распределения с использованием y[:] = sqrt.(x .+ 2)
2) Поддержка сокращений без распределения, таких как sum((x .- y).^2, 1)

y = √π exp(-x^2) * sin(k*x) + im * log(x-1)

Глядя на эти три примера из других, я думаю, что с синтаксисом for это закончится примерно так:
1) y[:] = [ sqrt(x + 2) for x ])
2) sum([ (x-y)^2 for x,y ], 1)
а также
y = [ √π exp(-x^2) * sin(k*x) + im * log(x-1) for x,k ]

Мне это очень нравится! Тот факт, что он создает временный массив, довольно очевиден, и он по-прежнему удобочитаем и лаконичен.

Небольшой вопрос, однако, может ли x[:] = [ ... for x ] иметь какую-то магию для изменения массива без выделения временного?
Я не уверен, что это принесет большую пользу, но я могу представить, что это поможет для больших массивов.
Я могу поверить, однако, что это может быть совершенно другой котел с рыбой, который должен обсуждаться в другом месте.

@Mike43110 Ваш x[:] = [ ... for x ] может быть записан как x[:] = (... for x) , RHS создаст генератор, который будет собираться поэлементно для заполнения x без выделения копии. Это была идея моего эксперимента LazyArray выше.

Синтаксис [f <- y] был бы удобен в сочетании с синтаксисом Int[f <- y] для карты, которая знает свой выходной тип и не нуждается в интерполяции из f(y[1]) того, чем будут другие элементы.

Тем более, что это дает интуитивно понятный интерфейс и для mapslices , [f <- rows(A)] где rows(A) (или columns(A) или slices(A, dims) ) возвращает Slice Объект

map(f, slice::Slice) = mapslices(f, slice.A, slice.dims)

Когда вы добавляете индексацию, это становится немного сложнее. Например

f(x[:,j]) .* g(x[i,:])

Трудно сравниться с ним по лаконичности. Вздутие от понимания стиля довольно плохое:

[f(x[m,j])*g(x[i,n]) for m=1:size(x,1), n=1:size(x,2)]

где, что еще хуже, требовалось сообразительность, чтобы знать, что это случай вложенной итерации, и ее нельзя сделать с помощью одного over . Хотя, если f и g немного дороже, это может быть быстрее:

[f(x[m,j]) for m=1:size(x,1)] .* [g(x[i,n]) for _=1, n=1:size(x,2)]

но еще дольше.

Этот пример, кажется, приводит доводы в пользу «точки», поскольку это может дать f.(x[:,j]) .* g.(x[i,:]) .

@JeffBezanson Я не уверен, какова цель вашего комментария. Кто-нибудь предлагал избавиться от синтаксиса .* ?

Нет; Здесь я сосредоточусь на f и g . Это пример, когда вы не можете просто добавить over x в конце строки.

Хорошо, я вижу, я пропустил конец комментария. Действительно, версия с точками лучше в этом случае.

Хотя с представлениями массива будет достаточно эффективная (AFAICT) и не такая уродливая альтернатива:
[ f(y) * g(z) for y in x[:,j], z in x[i,:] ]

Можно ли решить приведенный выше пример, вложив ключевые слова?

f(x)*g(y) over x,y

интерпретируется как

[f(x)*g(y) for (x,y) = zip(x,y)]

в то время как

f(x)*g(y) over x over y

становится

[f(x)*g(y) for x=x, y=y]

Тогда конкретный пример выше будет выглядеть примерно так

f(x[:,n])*g(x[m,:]) over x[:,n] over x[m,:]

РЕДАКТИРОВАТЬ: Оглядываясь назад, это не так лаконично, как я думал.

@JeffBezanson Как насчет

f(x[:i,n]) * g(x[m,:i]) over i

дает эквивалент f.(x[:,n] .* g.(x[m,:]) . Новый синтаксис x[:i,n] означает, что i вводится локально как итератор по индексам контейнера x[:,n] . Не знаю, возможно ли это реализовать. Но это не кажется (на первый взгляд) ни уродливым, ни громоздким, и сам синтаксис дает ограничения для итератора, а именно 1:length(x[:,n]). Что касается ключевых слов, «over» может сигнализировать о том, что будет использоваться весь диапазон, тогда как «for» может использоваться, если пользователь хочет указать поддиапазон 1:length(x[:,n]):

f(x[:i,n]) * g(x[m,:i]) for i in 1:length(x[:,n])-1 .

@davidagold , :i уже означает символ i .

Ах да, хорошая мысль. Ну, пока точки - это честная игра, как насчет

f(x[.i,n]) * g(x[m,.i]) over i

где точка указывает, что i вводится локально как итератор по 1:length(x[:,n). Я предполагаю, что по сути это переключает точечную запись с изменения функций на изменение массивов, а точнее их индексов. Это спасет от «точечной ползучести», как заметил Джефф:

[ f(g(e^(x[m,.i]))) * p(e^(f(y[.i,n]))) over i ]

в отличие от

f.(g.(e.^(x[m,:]))) .* p.(e.^(f.(y[:,n])))

хотя я полагаю, что последний немного короче. [РЕДАКТИРОВАТЬ: также, если можно опустить over i , когда нет двусмысленности, тогда на самом деле синтаксис немного короче:

[ f(g(e^(x[m,.i]))) * p(e^(f(y[.i,n]))) ] ]

Одним из потенциальных преимуществ синтаксиса понимания является то, что он может допускать более широкий диапазон шаблонов поэлементных операций. Например, если синтаксический анализатор понял, что индексация с i в x[m, .i] неявно выполняется по модулю длины (x[m,:]), тогда можно было бы написать

[ f(x[.i]) * g(y[.j]) over i, j=-i ]

Чтобы умножить элементы x на элементы y в обратном порядке, то есть первый элемент x на последний элемент y и т. д. , можно было бы написать

[ f(x[.i]) * g(y[.j]) over i, j=i+1 ]

умножить i й элемент x на i+1 й элемент y (где последний элемент x будет умножен первым элементом y из-за того, что индексация понимается в этом контексте как длина по модулю (x)). И если p::Permutation переставляет (1, ..., длина (x)) можно написать

[ f(x[.i]) * g(y[.j]) over i, j=p(i) ]

умножить i й элемент x на p(i) й элемент y .

В любом случае, это всего лишь скромное мнение стороннего наблюдателя по совершенно спекулятивному вопросу. =p Я ценю время, которое кто-то тратит на это.

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

unvectorized_sum(a, b, c, d) = a + b + c + d
vectorized_sum = @super_vectorize(unvectorized_sum)

a = [1, 2, 3, 4]
b = [1, 2, 3]
c = [1, 2]
d = 1

A = [1, 2, 3, 4]
B = [1, 2, 3, 1]
C = [1, 2, 1, 2]
D = [1, 1, 1, 1]

vectorized_sum(a, b, c, d) = vectorized_sum(A, B, C, D) = [4, 7, 8, 8]

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

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

Вопрос о том, использовать ли @super_vectorize , зависит от пользователя. Также можно было бы давать предупреждения для различных случаев. Например, в Р,

c(1, 2, 3) + c(1, 2)
[1] 2 4 4
Warning message:
In c(1, 2, 3) + c(1, 2) :
  longer object length is not a multiple of shorter object length

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

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

Но этот выпуск посвящен разработке системы удаления @vectorize_1arg и @vectorize_2arg из Базы. Наша цель — удалить из языка векторизованные функции и заменить их более качественной абстракцией.

Например, переработка может быть записана как

[ A[i] + B[mod1(i,length(B))] for i in eachindex(A) ]

что для меня довольно близко к идеальному способу написания. Никто не должен строить его за вас. Основные вопросы: (1) можно ли сделать это более кратким, (2) как распространить его на другие типы контейнеров.

Глядя на предложение @davidagold , я подумал, нельзя ли использовать var: для таких вещей, где переменная будет именем перед двоеточием. Я видел, что этот синтаксис означает A[var:end] , поэтому он кажется доступным.

Тогда f(x[:,j]) .* g(x[i,:]) будет f(x[a:,j]) * g(x[i,b:]) for a, b , что не намного хуже.

Однако несколько двоеточий были бы немного странными.

f(x[:,:,j]) .* g(x[i,:,:]) -> f(x[a:,a:,j]) * g(x[i,b:,b:]) for a, b было моей первоначальной мыслью по этому поводу.

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

using DataFrames

a = [1, 2, 3]
b = 1
c = [1 2]
d = <strong i="6">@data</strong> [NA, 2, 3]

# coerce an array to a certain size using recycling
coerce_to_size = function(argument, dimension_extents...)

  # number of repmats needed, initialized to 1
  dimension_ratios = [dimension_extents...]

  for dimension in 1:ndims(argument)

    dimension_ratios[dimension] = 
      ceil(dimension_extents[dimension] / size(argument, dimension))
  end

  # repmat array to at least desired size
  if typeof(argument) <: AbstractArray
    rep_to_size = repmat(argument, dimension_ratios...)
  else
    rep_to_size = 
      fill(argument, dimension_ratios...)
  end

  # cut down array to exactly desired size
  dimension_ranges = [1:i for i in dimension_extents]
  dimension_ranges = tuple(dimension_ranges...)

  rep_to_size = getindex(rep_to_size, dimension_ranges...)  

end

recycle = function(argument_list...)

  # largest dimension in arguments
  max_dimension = maximum([ndims(i) for i in argument_list])
  # initialize dimension extents to 1
  dimension_extents = [1 for i in 1:max_dimension]

  # loop through argument and dimension
  for argument_index in 1:length(argument_list)
    for dimension in 1:ndims(argument_list[argument_index])
      # find the largest size for each dimension
      dimension_extents[dimension] = maximum([
        size(argument_list[argument_index], dimension),
        dimension_extents[dimension]
      ])
    end
  end

  expand_arguments = 
    [coerce_to_size(argument, dimension_extents...) 
     for argument in argument_list]
end

recycle(a, b, c, d)

mapply = function(FUN, argument_list...)
  argument_list = recycle(argument_list...)
  FUN(argument_list...)
end

mapply(+, a, b, c, d)

Понятно, что это не самый элегантный и быстрый код (я недавно переселился в R). Я не уверен, как перейти отсюда к макросу @vectorize .

РЕДАКТИРОВАТЬ: комбинированный резервный цикл
РЕДАКТИРОВАТЬ 2: отдельное принуждение к размеру. в настоящее время работает только для измерений 0-2.
РЕДАКТИРОВАТЬ 3: Один немного более элегантный способ сделать это - определить специальный тип массива с индексацией мода. То есть,

special_array = [1 2; 3 5]
special_array.dims = (10, 10, 10, 10)
special_array[4, 1, 9, 7] = 3

РЕДАКТИРОВАТЬ 4: Вещи, которые мне интересны, существуют, потому что это было трудно написать: n-мерное обобщение hcat и vcat? Способ заполнить n-мерный массив (соответствующий размеру заданного массива) списками или кортежами индексов каждой конкретной позиции? N-мерное обобщение repmat?

[пао: подсветка синтаксиса]

Вы действительно не хотите определять функции с синтаксисом foo = function(x,y,z) ... end в Julia, хотя он работает. Это создает непостоянную привязку имени к анонимной функции. В Julia нормой является использование универсальных функций, а привязки к функциям автоматически становятся постоянными. В противном случае вы получите ужасную производительность.

Не понимаю, зачем здесь нужен репмат. Массивы, заполненные индексом каждой позиции, также являются предупредительным знаком: нет необходимости использовать большой кусок памяти для представления такого небольшого количества информации. Я считаю, что такие методы действительно полезны только в языках, где все должно быть «векторизовано». Мне кажется, правильный подход — просто запустить цикл, в котором некоторые индексы преобразуются, как в https://github.com/JuliaLang/julia/issues/8450#issuecomment -111898906.

Да, это имеет смысл. Вот начало, но мне трудно понять, как выполнить цикл в конце, а затем сделать макрос @vectorize .

function non_zero_mod(big::Number, little::Number)
  result = big % little
  result == 0 ? little : result
end

function mod_select(array, index...)
  # just return singletons
  if !(typeof(array) <: AbstractArray) return array end
  # find a new index with moded values
  transformed_index = 
      [non_zero_mod( index[i], size(array, i) )
       for i in 1:ndims(array)]
  # return value at moded index
  array[transformed_index...]
end

function mod_value_list(argument_list, index...)
  [mod_select(argument, index...) for argument in argument_list]
end

mapply = function(FUN, argument_list...)

  # largest dimension in arguments
  max_dimension = maximum([ndims(i) for i in argument_list])
  # initialize dimension extents to 1
  dimension_extents = [1 for i in 1:max_dimension]

  # loop through argument and dimension
  for argument_index in 1:length(argument_list)
    for dimension in 1:ndims(argument_list[argument_index])
      # find the largest size for each dimension
      dimension_extents[dimension] = maximum([
        size(argument_list[argument_index], dimension),
        dimension_extents[dimension]
      ])
    end
  end

  # more needed here
  # apply function over arguments using mod_value_list on arguments at each position
end

В разговоре @JeffBezanson упомянул синтаксис sin(x) over x , почему бы не что-то вроде:
sin(over x) ? (или используйте какой-либо символ вместо over в качестве ключевого слова)

Как только это будет решено, мы также можем решить # 11872.

Надеюсь, я не опаздываю на вечеринку, но я просто хотел бы предложить +1 к предложению @davidagold по синтаксису. Это концептуально ясно, лаконично и кажется естественным для написания. Я не уверен, будет ли . лучшим идентифицирующим символом или насколько осуществимой будет фактическая реализация, но можно сделать доказательство концепции, используя макрос, чтобы попробовать его (по сути, как @devec , но может быть даже проще в реализации).

Он также имеет то преимущество, что «вписывается» в существующий синтаксис понимания массива:

result = [g(f(.i), h(.j)) over i, j]

против.

result = [g(f(_i), h(_j)) for _i in eachindex(i), _j in eachindex(j)]

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

over , range и window имеют некоторый предшествующий уровень техники в пространстве OLAP в качестве модификаторов итерации, это кажется последовательным.

Мне не нравится синтаксис . , так как это похоже на шум в линии.

$, возможно, непротиворечив, интернировать значения итерации i, j в выражение?

result = [g(f($i), h($j)) over i, j]

Для автоматической векторизации выражения мы не можем taint один из векторов в выражении и заставить систему типов поднять выражение в векторное пространство?

Я делаю аналогичные операции с временными рядами, где выразительность julia уже позволяет мне писать

ts_a = GetTS( ... )
ts_b = GetTS( ... ) 
factors = [ 1,  2, 3 ]

ts_x = ts_a * 2 + sin( ts_a * factors ) + ts_b 

который при наблюдении выводит временной ряд векторов.

Основная недостающая часть — это возможность автоматического переноса существующих функций в пространство. Это надо делать руками

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

abstract TS{K}
function {F}{K}( x::TS{K}, y::TS{K} ) = tsjoin( F, x, y ) 
# tsjoin is a time series iteration operator

а затем иметь возможность специализироваться на конкретных операциях

function mean{K}(x::TS{K}) = ... # my hand rolled form

Привет @JeffBezanson ,

Если я правильно понимаю, я хотел бы предложить решение вашего комментария на JuliaCon 2015 относительно комментария, сделанного выше:
«[...] И говорить разработчикам библиотек, чтобы они использовали @vectorize для всех соответствующих функций, глупо; вы должны иметь возможность просто написать функцию, и если кто-то хочет вычислить ее для каждого элемента, он использует карту».
(Но я не буду затрагивать другой фундаментальный вопрос: «[..] нет действительно убедительной причины, по которой sin, exp и т. д. должны неявно отображаться в массивах».)

В Julia v0.40 мне удалось получить решение несколько лучше (на мой взгляд), чем @vectrorize :

abstract Vectorizable{Fn}
#Could easily have added extra argument to Vectorizable, but want to show inheritance case:
abstract Vectorizable2Arg{Fn} <: Vectorizable{Fn}

call{F}(::Type{Vectorizable2Arg{F}}, x1, x2) = eval(:($F($x1,$x2)))
function call{F,T1,T2}(fn::Type{Vectorizable2Arg{F}}, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

#Function in need of vectorizing:
function _myadd(x::Number, y::Number)
    return x+y+1
end

#"Register" the function as a Vectorizable 2-argument (alternative to @vectorize):
typealias myadd Vectorizable2Arg{:_myadd}

<strong i="13">@show</strong> myadd(5,6)
<strong i="14">@show</strong> myadd(collect(1:10),collect(21:30.0)) #Type stable!

Это более-менее разумно, но чем-то похоже на решение @vectorize . Чтобы векторизация была элегантной, я предлагаю Джулии поддерживать следующее:

abstract Vectorizable <: Function
abstract Vectorizable2Arg <: Vectorizable

function call{T1,T2}(fn::Vectorizable2Arg, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

#Note: by default, functions would normally be <: Function:
function myadd(x::Number, y::Number) <: Vectorizable2Arg
    return x+y+1
end

Вот и все! Наличие функции, наследуемой от функции Vectorizable, сделало бы ее векторизуемой.

Я надеюсь, что это соответствует тому, что вы искали.

С уважением,

Массачусетс

В отсутствие множественного наследования, как функция наследует от Vectorizable и от чего-то еще? И как вы связываете информацию о наследовании конкретных методов с информацией о наследовании универсальной функции?

@ma-laforge Вы уже можете это сделать --- определите тип myadd <: Vectorizable2Arg , затем реализуйте call для myadd на Number .

Спасибо за это @JeffBezanson!

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

abstract Vectorizable
#Could easily have parameterized Vectorizable, but want to show inheritance case:
abstract Vectorizable2Arg <: Vectorizable

function call{T1,T2}(fn::Vectorizable2Arg, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

#SECTION F: Function in need of vectorizing:
immutable MyAddType <: Vectorizable2Arg; end
const myadd = MyAddType()
function call(::MyAddType, x::Number, y::Number)
    return x+y+1
end

<strong i="7">@show</strong> myadd(5,6)
<strong i="8">@show</strong> myadd(collect(1:10),collect(21:30.0)) #Type stable

Теперь единственное, чего не хватает, — это способ «подтипа» любой функции, чтобы весь этот раздел F можно было заменить более элегантным синтаксисом:

function myadd(x::Number, y::Number) <: Vectorizable2Arg
    return x+y+1
end

ПРИМЕЧАНИЕ. Я сделал тип «MyAddType» и имя функции в одноэлементном объекте «myadd», потому что считаю результирующий синтаксис более приятным, чем если бы кто-то использовал Type{Vectorizable2Arg} в сигнатуре вызова:

function call{T1,T2}(fn::Type{Vectorizable2Arg}, v1::Vector{T1}, v2::Vector{T2})

К сожалению , судя по вашему ответу, это _не_ было бы адекватным решением "глупости" макроса @vectorize .

С уважением,

Массачусетс

@johnmyleswhite :

Я хотел бы ответить на ваш комментарий, но я не думаю, что понял. Вы можете уточнить?

Одно я _могу_ сказать:
В «Vectorizable» нет ничего особенного. Идея состоит в том, что любой может определить свой собственный «класс» функции (пример: MyFunctionGroupA<:Function ). Затем они могли бы перехватывать вызовы функций этого типа, определяя свою собственную сигнатуру «вызова» (как показано выше).

Сказав это: я предлагаю, чтобы функции, определенные в Base, использовали Base.Vectorizable <: Function (или что-то подобное), чтобы автоматически генерировать векторизованные алгоритмы.

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

myfunction(x::MyType, y::MyType) <: Base.Vectorizable

Конечно, им придется предоставить свою собственную версию promote_type(::Type{MyType},::Type{MyType}) , если по умолчанию уже не возвращается MyType .

Если алгоритма векторизации по умолчанию недостаточно, ничто не мешает пользователю реализовать собственную иерархию:

MyVectorizable{nargs} <: Function
call(fn::MyVectorizable{2}, x, y) = ...

myfunction(x::MyType, y:MyType) <: MyVectorizable{2}

Массачусетс

@ma-laforge, извините за неясность. Меня беспокоит то, что в любой иерархии всегда будет не хватать важной информации, потому что у Джулии одиночное наследование, которое требует, чтобы вы фиксировали один родительский тип для каждой функции. Если вы используете что-то вроде myfunction(x::MyType, y::MyType) <: Base.Vectorizable , то ваша функция не выиграет от того, что кто-то еще определит концепцию, такую ​​​​как Base.NullableLiftable , которая автоматически генерирует функции с нулевыми значениями.

Похоже, это не проблема с трейтами (см. https://github.com/JuliaLang/julia/pull/13222). Также связана новая возможность объявления методов чистыми (https://github.com/JuliaLang/julia/pull/13555), что может автоматически подразумевать, что такой метод является векторизуемым (по крайней мере, для методов с одним аргументом).

@johnmyleswhite ,

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

На самом деле я мало что знаю о NullableLiftables (кажется, этого нет в моей версии Джулии). Однако, предполагая, что он наследуется от Base.Function (что также невозможно в моей версии Джулии):

NullableLiftable <: Function

Затем ваш модуль может реализовать (только один раз) _new_ векторизуемый подтип:

abstract VectorizableNullableLiftable <: NullableLiftable

function call{T1,T2}(fn::VectorizableNullableLiftable, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

Итак, отныне любой, кто определяет функцию <: VectorizableNullableLiftable , будет автоматически применять ваш код векторизации!

function mycooladdon(scalar1, scalar2) <: VectorizableNullableLiftable
...

Я понимаю, что наличие более одного векторизируемого типа по-прежнему несколько болезненно (и немного неэлегантно)... Но, по крайней мере, это уберет одно из надоедливых повторений (1) в Джулии (необходимость регистрировать _каждый_ вновь добавленный функцией с вызовом @vectorize_Xarg).

(1) Это предполагает, что Джулия поддерживает наследование функций (например: myfunction(...)<: Vectorizable ) — чего не видно в v0.4.0. Решение, которое я получил, работая в Julia 0.4.0, - это просто хак... Вам все равно нужно зарегистрировать свою функцию... не намного лучше, чем вызов @vectorize_Xarg

Массачусетс

Я все еще думаю, что это какая-то неправильная абстракция. Функция, которая может или должна быть «векторизована», не является функцией определенного типа. Функция _Every_ может быть передана в map , придав ей такое поведение.

Кстати, с изменением, над которым я работаю в ветке jb/functions, вы сможете сделать function f(x) <: T (хотя, очевидно, только для первого определения f ).

Хорошо, я думаю, что лучше понимаю, что вы ищете... и это не то, что я предложил. Я думаю, что это может быть частью проблем, с которыми @johnmyleswhite столкнулся с моими предложениями...

...Но если я теперь понимаю, в чем проблема, решение кажется мне еще проще:

function call{T1,T2}(fn::Function, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

myadd(x::Number, y::Number) = x+y+1

Поскольку myadd имеет тип Function , он должен попасть в ловушку с помощью функции call ... что она и делает:

call(myadd,collect(1:10),collect(21:30.0)) #No problem

Но call не выполняет автоматическую отправку функций по какой-то причине (не знаю почему):

myadd(collect(1:10),collect(21:30.0)) #Hmm... Julia v0.4.0 does not dispatch this to call...

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

Что-то странное, что я заметил: Джулия уже автоматически векторизует функции, если они не типизированы:

myadd(x,y) = x+y+1 #This gets vectorized automatically, for some reason

RE: Кстати...:
Прохладный! Интересно, какие изящные вещи я смогу сделать, подтипируя функции :).

Джулия уже автоматически векторизует функции, если они не типизированы

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

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

Я разделяю ваши оговорки. У вас не может быть разумного определения, которое говорит «вот как вызвать любую функцию», потому что каждая функция сама говорит, что делать, когда она вызывается — вот что такое функция!

Я должен был сказать: ... определяемые пользователем инфиксные операторы, отличные от юникода, поскольку я не думаю, что мы хотим требовать от пользователей ввода символов юникода для доступа к таким основным функциям. Хотя я вижу, что $ на самом деле является одним из добавленных, так что спасибо за это! Вау, так это действительно работает в Джулии сегодня (даже если это не "быстро"... пока):

Юлия> ($) = карта
юлия> sin$(0.5*(abs2$(xy)))

@binarybana как насчет / \mapsto ?

julia> x, y = rand(3), rand(3);

julia> ↦ = map    # \mapsto<TAB>
map (generic function with 39 methods)

julia> sin ↦ (0.5 * (abs2 ↦ (x-y)))
3-element Array{Float64,1}:
 0.271196
 0.0927406
 0.0632608

Это также:

FWIW, я бы, по крайней мере, изначально предположил, что \mapsto был альтернативным синтаксисом для лямбда-выражений, поскольку он обычно используется в математике и, по сути, (в его воплощении ASCII, -> ) в Джулии тоже . Я думаю, это было бы довольно запутанно.

Говоря о математике… В теории моделей я видел, как map выражается применением функции к кортежу без круглых скобок. То есть, если \bar{a}=(a_1, \dots, a_n) , то f(\bar{a}) равно f(a_1, \dots, a_n) (т. е., по существу, apply ), а f\bar{a} равно (f(a_1), \dots, f(a_n)) (т. е map ). Полезный синтаксис для определения гомоморфизмов и т. д., но не все так просто перенести на язык программирования :-}

Как насчет любой другой альтернативы, такой как \Mapsto , вы не перепутаете ее с => (пара)? Я думаю, что оба символа различимы здесь рядом:

  • ->

Есть много символов, которые выглядят одинаково, зачем их так много, если мы используем только те, которые выглядят очень по-разному или представляют собой чистый ASCII?

Я думаю, это было бы довольно запутанно.

Я думаю, что документация и опыт решают эту проблему, вы согласны?

Есть также много других символов, похожих на стрелки, я, честно говоря, не знаю, для чего они используются в математике или где-то еще, я предложил только эти, потому что в их именах есть map ! :улыбка:

Я думаю, моя точка зрения заключается в том, что -> — это попытка Джулии представить в ASCII. Таким образом, использование для обозначения чего-то другого кажется опрометчивым. Дело не в том, что я не могу отличить их друг от друга визуально :-)

Я интуитивно чувствую, что если мы собираемся использовать устоявшиеся математические символы, мы могли бы хотя бы подумать о том, как использование Джулии отличается от устоявшегося использования. Кажется логичным выбирать символы с map в их именах, но в данном случае это относится к определению карты (или карт разных типов, или типов таких карт). Это также относится к использованию в Pair, более или менее, где вместо того, чтобы определять полную функцию, определяя, на что сопоставляется параметр, вы фактически перечисляете, на что сопоставляется данный аргумент (значение параметра), т. е. это элемент явного перечисляемая функция, концептуально (например, словарь).

@Ismael-VC Проблема с вашим предложением заключается в том, что вам нужно дважды вызвать map , в то время как идеальное решение не будет включать временные массивы и будет сокращено до map((a, b) -> sin(0.5 * abs2(a-b)), x, y) . Кроме того, повторение дважды не очень удобно как для визуального восприятия, так и для набора текста (для которого было бы неплохо иметь эквивалент ASCII).

Пользователям R может не понравиться эта идея, но если мы перейдем к отказу от текущего синтаксического анализа специального случая инфиксных макросов ~ (такие пакеты, как GLM и DataFrames, должны будут перейти на макросинтаксический анализ своих формул DSL, ссылка https:/ /github.com/JuliaStats/GLM.jl/issues/116), что освободило бы редкий товар в виде инфиксного оператора ascii.

a ~ b может быть определено в базе как map(a, b) , и, возможно, a .~ b может быть определено как broadcast(a, b) ? Если он анализируется как обычный инфиксный оператор, то макрос DSL, подобный эмуляции интерфейса формулы R, может свободно реализовывать свою собственную интерпретацию оператора внутри макросов, как это делает JuMP с <= и == .

Возможно, это не самое красивое предложение, но и сокращения в Mathematica, если вы ими злоупотребляете... мой любимый .#&/@

:+1: для менее специального регистра и большей общности и согласованности значения, которые вы предлагаете для ~ и .~ , мне кажутся великолепными.

+1 Тони.

@tkelman Чтобы было ясно, как бы вы написали, например, sin(0.5 * abs2(a-b)) полностью векторизованным способом?

С пониманием, наверное. Я не думаю, что инфиксная карта будет работать для varargs или на месте, поэтому возможность свободного синтаксиса не решает всех проблем.

Так что это не решит эту проблему. :-/

Пока синтаксис sin(0.5 * abs2(a-b)) over (a, b) (или его вариант, возможно, с использованием инфиксного оператора) является наиболее привлекательным.

Название этого выпуска: «Альтернативный синтаксис для map(func, x) ». Использование инфиксного оператора не решает слияние карты/цикла для устранения временных, но я думаю, что это может быть даже более широкая, связанная, но технически отдельная проблема, чем синтаксис.

Да, я согласен с @tkelman , дело в том, чтобы иметь альтернативный синтаксис для map , поэтому я предложил использовать \mapsto , . То, что упоминает @nalimilan , кажется более широким и лучше подходит для другой проблемы, ИМХО, How to fully vecotrize expressions ?

<rambling>
Эта проблема существует уже более года (и может продолжаться бесконечно, как и многие другие проблемы сейчас)! Но сейчас мы могли бы получить Alternative syntax for map(func, x) . Из ± 450 участников по юлианскому календарю только 41 смогли найти эту проблему и/или пожелали поделиться мнением (это много для проблемы с github, но явно недостаточно в данном случае), в целом не так уж много разных предложений (это не просто незначительные вариации одной и той же концепции).

Я знаю, что некоторым из вас не нравится эта идея или вы не видите ценности в проведении опросов/опросов (:shocked:), но поскольку мне не нужно ни у кого спрашивать разрешения на что-то подобное, я все равно это сделаю. Немного грустно, что мы не в полной мере используем возможности нашего сообщества, социальных сетей и других сообществ, а еще более грустно, что мы не видим в этом ценности, давайте посмотрим, смогу ли я собрать больше разных и свежих мнений или хотя бы проверить выясните, что думает большинство о нынешних мнениях по этому конкретному вопросу, в качестве эксперимента и посмотрите, как оно пойдет. Может быть, это действительно бесполезно, а может быть, и нет, есть только один способ узнать это по-настоящему.
</rambling>

@Ismael-VC: Если вы действительно хотите провести опрос, первое, что вы должны сделать, это тщательно обдумать вопрос, который вы хотите задать. Вы не можете ожидать, что все прочитают всю ветку и резюмируют варианты, которые обсуждались индивидуально.

map(func, x) также охватывает такие вещи, как map(v -> sin(0.5 * abs2(v)), x) , и это то, что обсуждалось в этой теме. Давайте не будем переносить это в другую тему, так как это затруднит удержание в памяти всех предложений, обсуждавшихся выше.

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

@Ismael-VC Опросы здесь вряд ли помогут, ИМХО. Мы не пытаемся выяснить, какое из нескольких решений является лучшим, а скорее пытаемся найти решение, которое еще никто не нашел. Это обсуждение уже долгое и в нем участвует много людей, я не думаю, что добавление большего количества поможет.

@Ismael-VC Все в порядке, не стесняйтесь провести опрос. На самом деле, в прошлом я провел пару дудл-опросов (например, http://doodle.com/poll/s8734pcue8yxv6t4). По моему опыту, в опросах голосует столько же или даже меньше людей, сколько обсуждают в тематических темах. Это имеет смысл для очень специфических, часто поверхностных/синтаксических проблем. Но как опрос может генерировать свежие идеи, когда все, что вы можете сделать, это выбрать из существующих вариантов?

Конечно, реальная цель этого выпуска состоит в том, чтобы исключить неявно векторизованные функции. Теоретически для этого достаточно синтаксиса для map , потому что все эти функции просто выполняют map в каждом случае.

Я пытался найти существующие математические обозначения для этого, но вы, как правило, видите комментарии о том, что операция слишком неважна, чтобы иметь обозначения! В случае произвольных функций в математическом контексте я почти могу в это поверить. Однако наиболее близкой вещью, по-видимому, является нотация продукта Адамара, которая имеет несколько обобщений: https://en.wikipedia.org/wiki/Hadamard_product_ (matrices)#Analogous_Operations

Это оставляет нам sin∘x , как предложил @rfourquet . Это не кажется слишком полезным, так как для этого требуется юникод, и в любом случае он малоизвестен.

@nalimilan , я думаю, вы бы просто сделали sin(0.5 * abs2(a-b)) ~ (a,b) , что переводится как map((a,b)->sin(0.5 * abs2(a-b)), (a,b)) . Не уверен, что это совсем правильно, но я думаю, что это сработает.

Я также опасаюсь слишком много вникать в проблему "дай-мне-тебе-огромное-сложное-выражение-и-ты-идеально-авто-векторизируй-его-для-меня". Я думаю, что окончательным решением в этом отношении является создание полной DAG выражений/задач + планирование запросов и т. д. Но я думаю, что это гораздо более крупная рыба, чем просто удобный синтаксис для map .

@quinnj Да, по сути, это синтаксис over , предложенный выше, за исключением инфиксного оператора.

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

@johnmyleswhite согласен, начинает выглядеть как DSL ака Linq

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

julia> (|>)(x::Function, y...) = map(x, y... )
|> (generic function with 8 methods)

julia> (|>)(x::Function, y::Function) = (z...)->x(y(z...))
|> (generic function with 8 methods)

julia> sin |> cos |> [ 1,2,3 ]
3-element Array{Float64,1}:
  0.514395
 -0.404239
 -0.836022

julia> x,y = rand(3), rand(3)
([0.8883630054185454,0.32542923024720194,0.6022157767415313],    [0.35274912207468145,0.2331784754319688,0.9262490059844113])

julia> sin |> ( 0.5 *( abs( x - y ) ) )
3-element Array{Float64,1}:
 0.264617
 0.046109
 0.161309

@johnmyleswhite Это правда, но есть достойные промежуточные цели, которые довольно скромны. В моей ветке map версия векторизованных выражений с несколькими операциями уже работает быстрее, чем то, что мы имеем сейчас. Поэтому выяснить, как плавно перейти к нему, довольно срочно.

@johnmyleswhite Не уверен. Большая часть SQL связана с выбором, упорядочением и объединением строк. Здесь мы говорим только о применении функции поэлементно. Кроме того, SQL не предоставляет никакого синтаксиса, позволяющего отличить сокращения (например, SUM ) от поэлементных операций (например, > , LN ). Последние просто автоматически векторизуются, как сейчас в Джулии.

@JeffBezanson Прелесть использования \circ заключается в том, что если вы интерпретируете индексированное семейство как функцию из набора индексов (что является стандартной математической «реализацией»), то отображение _is_ просто композиция. Итак, (sin ∘ x)(i)=sin(x(i)) , а точнее sin(x[i]) .

Использование канала, как упоминает @mdcfrancis , по сути, будет просто композицией «порядок диаграммы», которая часто выполняется с (возможно, толстой) точкой с запятой в математике (или особенно в приложениях CS теории категорий), но у нас уже есть канал Оператор, конечно.

Если ни один из этих операторов композиции не подходит, то можно использовать некоторые другие. Например, по крайней мере, некоторые авторы используют непритязательную \cdot для абстрактной композиции стрелок/морфизмов, поскольку по сути это «умножение» группоида (более или менее) стрелок.

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

Таким образом, можно было бы получить sin . x … но я думаю, это сбило бы с толку :-}

Тем не менее… эта последняя аналогия может быть аргументом в пользу одного из действительно ранних предложений, т. е. sin.(x) . (Или, может быть, это надумано.)

Давай попробуем с другого ракурса, не стреляй в меня.

Если мы определим .. как collect(..(A,B)) == ((a[1],..., a[n]), (b[1], ...,b[n])) == zip(A,B) , то используя T[x,y,z] = [T(x), T(y), T(z)] формально, будет верным, что

map(f,A,B) = [f(a[1],b[1]), ..., f(a[n],b[n])] = f[zip(A,B)...] = f[..(A,B)]

Это мотивирует по крайней мере один синтаксис для карты, который не мешает синтаксису построения массива. С :: или table расширение f[::(A,B)] = [f(a[i], b[j]) for i in 1:n, j in 1:n] приводит, по крайней мере, ко второму интересному варианту использования.

внимательно обдумайте вопрос, который хотите задать.

@toivoh Спасибо, буду. В настоящее время я оцениваю несколько программ для опросов/опросов. Кроме того, я буду опрашивать только предпочтительный синтаксис , те, кто хочет прочитать всю ветку, просто сделают это, давайте просто не будем предполагать, что никто другой не будет заинтересован в этом.

найти решение, которое еще никто не нашел

@nalimilan никто среди нас, то есть. :улыбка:

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

@JeffBezanson Я рад слышать, что вы уже проводили опросы, так держать!

  • Как вы продвигаете свои опросы?
  • Из программного обеспечения для опросов / опросов, которое я оценил до сих пор, kwiksurveys.com позволяет пользователям добавлять свои собственные мнения вместо _ни одной из этих опций для меня_.

sin∘x Не кажется слишком полезным, так как требует Unicode и в любом случае не широко известен.

У нас так много Unicode, давайте использовать его, у нас даже есть хороший способ использовать их с завершением табуляции, что плохого в их использовании тогда? изобретать это? Действительно ли нам нужно ждать, пока кто-то другой изобретет и использует его, чтобы мы могли принять его как прецедент и только после этого рассматривать?

имеет прецедент, так что проблема в том, что это Unicode? Зачем? когда же мы начнем использовать остальную часть малоизвестного Unicode? никогда?

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

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

Было бы совершенно безумием использовать/перегружать * в качестве альтернативы ASCII? Я бы сказал, что можно утверждать, что это имеет какой-то математический смысл, но я полагаю, что иногда может быть трудно понять его смысл… (Опять же, если это ограничено функциональностью map , тогда map уже является альтернативой ASCII, не так ли?)

Красота использования \circ заключается в том, что если вы интерпретируете индексированное семейство как функцию от набора индексов (что является стандартной математической «реализацией»), то отображение _является_ просто композицией.

Я не уверен, что куплю это.

@hayd Какая часть этого? Что индексированное семейство (например, последовательность) можно рассматривать как функцию от набора индексов или что отображение над ним становится композицией? Или что это полезная перспектива в данном случае?

Я думаю, что первые два (математических) пункта довольно бесспорны. Но, да, я не собираюсь решительно выступать за использование этого здесь — это было в основном «Ах, вроде как подходит!» реакция.

@mlhetland |> довольно близок к -> и работает сегодня - у него также есть «преимущество» в том, что он правильно ассоциативен.

x = parse( "sin |> cos |> [1,2]" )
:((sin |> cos) |> [1,2])

@mdcfrancis Конечно. Но это переворачивает изложенную мной интерпретацию композиции с ног на голову. То есть sin∘x будет эквивалентно x |> sin , нет?

PS: Возможно, это потерялось в «алгебре», но простое разрешение функций в построении типизированного массива T[x,y,z] , таких что f[x,y,z] равно [f(x),f(y),f(z)] , дает напрямую

map(f,A) == f[A...]

что вполне читабельно и может рассматриваться как синтаксис.

Это умно. Но я подозреваю, что если мы сможем заставить это работать, sin[x...] действительно проигрывает по многословности sin(x) или sin~x и т. д.

А как насчет синтаксиса [sin xs] ?

По синтаксису это похоже на понимание массива [sin(x) for x in xs] .

@mlhetland sin |> x === map( sin, x )

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

@mdcfrancis Да, я понимаю, это то, к чему вы стремитесь. Что меняет положение вещей (как повторяет @tkelman ) относительно. интерпретация композиции, которую я изложил.

Я думаю, что интеграция векторизации и цепочки была бы довольно крутой. Интересно, будут ли слова самыми четкими операторами?
Что-то типа:

[1, 2] mapall
  +([2, 3]) map
  ^(2, _) chain
  { a = _ + 1
    b = _ - 1
    [a..., b...] } chain
  sum chain
  [ _, 2, 3] chain
  reduce(+, _)

Несколько карт подряд могут быть автоматически объединены в одну карту для повышения производительности. Также обратите внимание, что я предполагаю, что карта будет иметь какую-то функцию автоматической трансляции. Замена [1, 2] на _ в начале может вместо этого создать анонимную функцию. Примечание. Я использую правила R magrittr для создания цепочек (см. мой пост в ветке цепочки).

Возможно, это начинает больше походить на DSL.

Я давно слежу за этой проблемой и до сих пор не комментировал, но ИМХО это начинает выходить из-под контроля.

Я решительно поддерживаю идею чистого синтаксиса для карты. Мне больше всего нравится предложение @tkelman о ~ , поскольку оно соответствует ASCII для такой базовой функциональности, и мне очень нравится sin~x . Это позволило бы создать довольно сложное отображение в однострочном стиле, как обсуждалось выше. Можно также использовать sin∘x . Для чего-то более сложного я склонен думать, что правильная петля просто намного понятнее (и, как правило, лучшая производительность). Мне не очень нравится слишком много «магического» вещания, это значительно усложняет следование коду. Явный цикл обычно понятнее.

Это не значит, что такую ​​функциональность не следует добавлять, но давайте сначала создадим хороший краткий синтаксис map , тем более, что он скоро станет очень быстрым (из моих тестов ветки jb/functions ) .

Обратите внимание, что одним из эффектов jb/functions является то, что broadcast(op, x, y) имеет такую ​​же хорошую производительность, как и настроенная версия x .op y , которая вручную специализировала вещание на op .

Для чего-то более сложного я склонен думать, что правильная петля просто намного понятнее (и, как правило, лучшая производительность). Мне не очень нравится слишком много «магического» вещания, это значительно усложняет следование коду. Явный цикл обычно понятнее.

Я не согласен. exp(2 * x.^2) отлично читается и менее многословен, чем [exp(2 * v^2) for v in x] . Задача здесь, ИМХО, состоит в том, чтобы не заманивать людей в ловушку, позволяя им использовать первый (который выделяет копии и не объединяет операции): для этого нам нужно найти синтаксис, который был бы достаточно коротким, чтобы медленная форма могла быть объявлена ​​устаревшей.

Больше мыслей. Есть несколько возможных вещей, которые вы, возможно, захотите сделать при вызове функции:

цикл без аргументов (цепочка)
перебирать только связанный аргумент (карта)
цикл по всем аргументам (mapall)

Каждое из вышеперечисленного может быть изменено:
Пометка элемента для цикла (~)
Пометка элемента, который не будет зацикливаться (дополнительный набор [ ] )

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

Вещание имеет значение только тогда, когда было бы измерение
несоответствие в противном случае. Поэтому, когда вы говорите не транслировать, вы имеете в виду дать
ошибка вместо этого, если размер аргумента не совпадает?

sin[x...] действительно проигрывает по многословности sin(x) или sin~x и т.д.

Также, продолжая мысль, карта sin[x...] является менее рьяной версией на [f(x...)] .
Синтаксис

[exp(2 * (...x)^2)]

или что-то подобное, например [exp(2 * (x..)^2)] , будет доступно и самоочевидно, если когда-либо будет введена настоящая цепочка неявных функций.

@nalimilan да, но это вписывается в мою категорию «однострочников», которые, как я сказал, подходят без цикла.

Пока мы перечисляем все наши пожелания: для меня гораздо важнее, чтобы результаты map можно было присваивать без распределения или копирования. Это еще одна причина, по которой я по-прежнему предпочитаю циклы для критически важного для производительности кода, но если это можно смягчить (№ 249 в настоящее время не выглядит обнадеживающим ATM), тогда все это становится гораздо более привлекательным.

результаты карты могут быть назначены без выделения или копирования

Можете ли вы немного расширить это? Вы, конечно, можете изменить результат map .

Я предполагаю, что он имеет в виду сохранение вывода map в предварительно выделенный массив.

Да, точно. Извините, если это уже возможно.

Ах, конечно. У нас есть map! , но, как вы заметили, #249 запрашивает более приятный способ сделать это.

@jtravs Я предложил решение выше с LazyArray (https://github.com/JuliaLang/julia/issues/8450#issuecomment-65106563), но пока производительность не была идеальной.

@toivoh Я внес несколько правок в этот пост после того, как опубликовал его. Вопрос, который меня беспокоил, заключается в том, как выяснить, какие аргументы следует использовать в цикле, а какие нет (поэтому mapall может быть понятнее, чем широковещательная передача). Я думаю, что если вы перебираете более одного аргумента, всегда следует при необходимости расширять одноэлементные размеры для создания сопоставимых массивов.

Да, map! совершенно верно. Было бы хорошо, если бы любой хороший синтаксический сахар, разработанный здесь, также охватывал этот случай. Могли бы мы не иметь, что x := ... неявно отображает RHS на x .

Я установил пакет под названием ChainMap, который объединяет сопоставление и цепочку.

Вот краткий пример:

<strong i="7">@chain</strong> begin
  [1, 2]
  -(1)
  (_, _)
  map_all(+)
  <strong i="8">@chain_map</strong> begin
    -(1)
    ^(2. , _)
  end
  begin
    a = _ - 1
    b = _ + 1
    [a, b]
  end
  sum
end

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

  1. Начиная с f[a...] , который фактически был предложен @Jutho , соглашение
    что для векторов a, b
f[a...] == map(f, a[:])
f[a..., b...] == map(f, a[:], b[:])
etc

который не вводит новые символы.

2.) Кроме того, я бы предложил ввести еще один оператор: _сохраняющий_ форму оператор разбрызгивания .. (скажем). Это связано с тем, что ... является _плоским_ оператором разделения, поэтому f[a...] должен возвращать вектор, а не массив, даже если a является n -мерным. Если выбрано .. , то в этом контексте

f[a.., ] == map(f, a)
f[a.., b..] == map(f, a, b)

и результат наследует форму аргументов. Разрешение трансляции

f[a.., b..] == broadcast(f, a, b)

позволит писать думает, как

sum(*[v.., v'..]) == dot(v,v)

Эврика?

Это не помогает с выражениями сопоставления, не так ли? Одним из преимуществ синтаксиса over является то, как он работает с выражениями:

sin(x * (y - 2)) over x, y  == map((x, y) -> sin(x * (y - 2)), x, y) 

Ну, возможно, через [sin(x.. * y..)] или sin[x.. * y..] выше, если вы хотите это разрешить. Мне это нравится немного больше, чем синтаксис over, потому что он дает визуальный намек на то, что функция работает с элементами, а не с контейнерами.

Но нельзя ли упростить это так, чтобы x.. просто отображалось на x ? Итак, пример @johansigfrids :

sin(x.. * (y.. - 2))  == map((x, y) -> sin(x * (y - 2)), x, y)

@jtravs Из-за области видимости ( [println(g(x..))] против println([g(x..)]) ) и согласованности [x..] = x .

Еще одна возможность состоит в том, чтобы взять x.. = x[:, 1], x[:, 2], etc. в качестве частичного знака ведущих подмассивов (столбцов) и ..y в качестве частичного знака конечных подмассивов ..y = y[1,:], y[2,:] . Если оба работают с разными индексами, это охватывает много интересных случаев.

[f(v..)] == [f(v[i]) for i in 1:m ]
[v.. * v..] == [v[i] * v[i] for 1:m]
[v.. * ..v] == [v[i] * v[j] for i in 1:m, j in 1:n]
[f(..A)] == [f(A[:, j]) for j in 1:n]
[f(A..)] == [f(A[i, :]) for i in 1:m]
[dot(A.., ..A)] == [dot(A[:,i], A[j,:]) for i in 1:m, j in 1:n] == A*A
[f(..A..)] == [f(A[i,j]) for i in 1:m, j in 1:n]
[v..] == [..v] = v
[..A..] == A

( v вектор, A матрица)

Я предпочитаю over , так как это позволяет вам написать выражение в обычном синтаксисе вместо того, чтобы вводить множество квадратных скобок и точек.

Вы правы насчет беспорядка, думаю я, и я попытался адаптировать и систематизировать свое предложение. Чтобы не перенапрягать всеобщее терпение, я написал свои мысли о картах, индексах и т. д. в кратком виде https://gist.github.com/mschauer/b04e000e9d0963e40058 .

После прочтения этой ветки я предпочел бы иметь _оба_ f.(x) для простых вещей и людей, привыкших к векторизованным функциям (довольно распространена идиома " . = vectorized"), и f(x^2)-x over x для более сложных выражений.

Слишком много людей приходят из Matlab, Numpy и так далее, чтобы полностью отказаться от синтаксиса векторизованных функций; сказать им, чтобы добавить точки легко запомнить. Хороший over -подобный синтаксис для векторизации сложных выражений в один цикл также очень полезен.

Синтаксис over действительно раздражает меня. Мне только что пришло в голову, почему: предполагается, что все использования каждой переменной в выражении векторизованы или не векторизованы, что может быть не так. Например, log(A) .- sum(A,1) — предположим, что мы удалили векторизацию log . Вы также не можете векторизовать функции над выражениями, что кажется довольно серьезным недостатком, что, если бы я хотел написать exp(log(A) .- sum(A,1)) и иметь векторизованные exp и log и sum нет?

@StefanKarpinski , тогда вы должны сделать либо exp.(log.(A) .- sum(A,1)) и принять дополнительные временные файлы (например, при интерактивном использовании, где производительность не критична), либо s = sum(A, 1); exp(log(A) - s) over A (хотя это не совсем правильно, если sum(A,1) — это вектор, и вы хотели трансляцию); возможно, вам просто придется использовать понимание. Независимо от того, какой синтаксис мы придумаем, мы не собираемся охватывать все возможные случаи, и ваш пример особенно проблематичен, потому что любой «автоматизированный» синтаксис должен знать, что sum является чистым и что его можно поднят из цикла/карты.

Для меня первоочередной задачей является синтаксис f.(x...) для broadcast(f, x...) или map(f, x...) , чтобы мы могли избавиться от @vectorize . После этого мы можем продолжить работу над синтаксисом вроде over (или любым другим), чтобы сократить более общие варианты использования map и вкрапления.

@stevengj Я не думаю, что второй пример работает, потому что - не будет транслироваться. Предполагая, что A является матрицей, на выходе будет матрица однострочных матриц, каждая из которых представляет собой логарифм элемента A минус вектор сумм по первому измерению. Вам понадобится broadcast((x, y)->exp(log(x)-y), A, sum(A, 1)) . Но я думаю, что краткий синтаксис для map полезен и не обязательно должен быть кратким синтаксис для broadcast .

Будут ли функции, которые исторически были автоматически векторизованы, такие как sin , останутся таковыми с новым синтаксисом, или это станет устаревшим? Я опасаюсь, что даже синтаксис f. покажется большой массе программистов-ученых, которые не мотивированы аргументами концептуальной элегантности, как «попалась».

Я считаю, что исторически векторизованные функции, такие как sin , должны быть объявлены устаревшими в пользу sin. , но они должны быть признаны устаревшими почти навсегда (в отличие от полного удаления в следующем выпуске) для пользу пользователей от других научных языков.

Одна небольшая (?) проблема с f.(args...) : хотя синтаксис object.(field) по большей части используется редко и, вероятно, может быть заменен на getfield(object, field) без особых усилий, есть _lot_ определений/ссылок методов в форме Base.(:+)(....) = .... , и было бы болезненно изменить их на getfield .

Одним из обходных путей может быть:

  • Пусть Base.(:+) превращается в map(Base, :+) , как и все остальные f.(args...) , но определяет устаревший метод map(m::Module, s::Symbol) = getfield(m, s) для обратной совместимости.
  • поддерживать синтаксис Base.:+ (который в настоящее время не работает) и рекомендовать его в предупреждении об устаревании для Base.(:+)

Я хотел бы еще раз спросить - можем ли мы сделать это в 0.5.0? Я думаю, что это важно из-за устаревания многих векторизованных конструкторов. Я думал, что меня это устроит, но я нахожу map(Int32, a) вместо int32(a) немного утомительным.

Это просто вопрос выбора синтаксиса на данный момент?

Это просто вопрос выбора синтаксиса на данный момент?

Я думаю, что @stevengj привел хорошие аргументы в пользу написания sin.(x) вместо .sin(x) в своем PR https://github.com/JuliaLang/julia/pull/15032. Так что я бы сказал, что путь был расчищен.

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

@JeffBezanson Я возвращаю веху в этом вопросе на 0.5.0, чтобы поднять ее во время обсуждения сортировки - в основном, чтобы убедиться, что я не забыл.

#15032 также работает для call - например, Int32.(x) ?

@ViralBSah , да. Любой f.(x...) преобразуется в map(f, broadcast, x...) на уровне синтаксиса, независимо от типа f .

Это главное преимущество . по сравнению с чем-то вроде f[x...] , которое в остальном привлекательно (и не требует изменений синтаксического анализатора), но будет работать только для f::Function . f[x...] также концептуально немного конфликтует с пониманием массива T[...] . Хотя я думаю, что @StefanKarpinski нравится синтаксис скобок?

(Другой пример: объекты o::PyObject в PyCall можно вызывать, вызывая метод __call__ объекта Python o , но те же объекты могут также поддерживать o[...] индексация. Это немного конфликтует с вещанием f[x...] , но будет нормально работать с вещанием o.(x...) .)

call больше не существует.

(Мне также нравится аргумент @nalimilan о том, что f.(x...) делает .( аналогом .+ и т. д.)

Да, точечная аналогия мне тоже нравится больше всего. Можем ли мы пойти дальше и объединиться?

Должно ли getfield с модулем быть фактически устаревшим?

@tkelman , в отличие от чего? Тем не менее, предупреждение об устаревании для Base.(:+) (т. е. аргументы буквального символа) должно предлагать Base.:+ , а не getfield . (_Update_: также требуется отказ от синтаксиса для обработки определений методов.)

@ViralBSah , было ли какое-либо решение по этому поводу на обсуждении сортировки в четверг? Я думаю, #15032 в довольно хорошей форме для слияния.

Я думаю, Вирал пропустил эту часть звонка. У меня сложилось впечатление, что многие люди все еще сомневаются в эстетике f.(x) и могут предпочесть либо

  1. инфиксный оператор, который был бы проще концептуально и в реализации, но у нас нет доступных ascii-операторов, насколько я вижу. Моя более ранняя идея отказаться от синтаксического анализа макросов ~ потребовала бы работы по замене в пакетах, и, вероятно, уже слишком поздно пытаться сделать это в этом цикле.
  2. или альтернативный новый синтаксис, упрощающий объединение циклов и устранение временных ошибок. Никакие другие альтернативы не были реализованы на уровне #15032, поэтому похоже, что мы должны объединить это и попробовать, несмотря на оставшиеся оговорки.

Да, у меня есть некоторые оговорки, но сейчас я не вижу лучшего варианта, чем f.(x) . Это кажется лучше, чем выбор произвольного символа, такого как ~ , и я уверен, что многие, кто привык к .* (и т. д.), могут даже сразу догадаться, что это означает.

Одна вещь, которую я хотел бы лучше понять, это то, согласны ли люди с _заменой_ существующих векторизованных определений на .( . Если людям не понравится это настолько, чтобы сделать замену, я бы больше колебался.

Как пользователь, участвующий в этом обсуждении, я ОЧЕНЬ хотел бы использовать это, чтобы заменить мой существующий векторизованный код.

Я в основном использую векторизацию в julia для удобочитаемости, поскольку циклы выполняются быстро. Так что мне нравится использовать его для получения опыта, греха и т. д., как было упомянуто ранее. Поскольку я уже буду использовать .^, .* в таких выражениях, добавляя лишнюю точку к греху. эксп. и т. д. кажется мне действительно естественным и даже более явным ... особенно, когда я могу затем легко сложить свои собственные функции с общей нотацией вместо того, чтобы смешивать sin (x) и map (f, x).

В общем, как обычный пользователь, я очень, очень надеюсь, что это будет объединено!

Мне больше нравится предложенный синтаксис fun[vec] , чем fun.(vec) .
Что вы думаете о [fun vec] ? Это похоже на понимание списка, но с неявной переменной. Это может позволить сделать T[fun vec]

Этот синтаксис бесплатен в Julia 0.4 для векторов с длиной> 1:

julia> [sin rand(1)]
1x2 Array{Any,2}:
 sin  0.0976151

julia> [sin rand(10)]
ERROR: DimensionMismatch("mismatch in dimension 1 (expected 1 got 10)")
 in cat_t at abstractarray.jl:850
 in hcat at abstractarray.jl:875

Что-то вроде [fun over vec] может быть преобразовано на уровне синтаксиса, и, возможно, стоит упростить [fun(x) for x in vec] , но не проще, чем map(fun,vec) .

Синтаксис, аналогичный [fun vec] : синтаксис (fun vec) является бесплатным, а {fun vec} устарел.

julia> (fun vec)
ERROR: syntax: missing separator in tuple

julia> {fun vec}

WARNING: deprecated syntax "{a b ...}".
Use "Any[a b ...]" instead.
1x2 Array{Any,2}:
 fun  [0.3231600663395422,0.10208482721149204,0.7964663210635679,0.5064134055014935,0.7606900072242995,0.29583012284224064,0.5501131920491444,0.35466150455688483,0.6117729165962635,0.7138111929010424]

@diegozea , fun[vec] было исключено, потому что оно конфликтует с T[vec] . (fun vec) в основном представляет собой синтаксис Scheme, а случай с несколькими аргументами предположительно равен (fun vec1 vec2 ...) ... это довольно не похоже на любой другой синтаксис Julia. Или вы имели в виду (fun vec1, vec2, ...) , что противоречит синтаксису кортежа? Также неясно, в чем будет преимущество над fun.(vecs...) .

Кроме того, помните, что главная цель состоит в том, чтобы в конечном итоге иметь синтаксис для замены функций @vectorized (чтобы у нас не было «благословенного» подмножества функций, которые «работают с векторами»), и это означает, что синтаксис должен быть приятным/интуитивно понятным/удобным для людей, привыкших к векторизованным функциям в Matlab, Numpy и так далее. Он также должен быть легко компонуемым для таких выражений, как sin(A .+ cos(B[:,1])) . Эти требования исключают множество более «творческих» предложений.

sin.(A .+ cos.(B[:,1])) выглядит не так уж и плохо. Для этого понадобится хорошая документация. Будет ли f.(x) документироваться как .( аналогично .+ ?
Можно ли отказаться от .+ в пользу +. ?

# Since 
sin.(A .+ cos.(B[:,1]))
# could be written as
sin.(.+(A, cos.(B[:,1])))
# +.
sin.(+.(A, cos.(B[:,1]))) #  will be more coherent.

@diegozea #15032 уже содержит документацию, но приветствуются любые дополнительные предложения.

.+ по-прежнему будет писаться как .+ . Во-первых, такое расположение точки слишком укоренилось, и здесь недостаточно выиграть, изменив написание. Во-вторых, как указал @nalimilan , вы можете думать о .( как о «векторизованном операторе вызова функции», и с этой точки зрения синтаксис уже согласован.

(Как только трудности с вычислением типов в broadcast (#4883) будут устранены, я надеюсь сделать еще один PR, чтобы a .⧆ b для любого оператора был просто сахаром. для вызова broadcast(⧆, a, b) . Таким образом, нам больше не нужно будет явно реализовывать .+ и т.д. — вы получите оператора вещания автоматически, просто определив + и т.д. по-прежнему иметь возможность реализовывать специализированные методы, например вызовы BLAS, путем перегрузки broadcast для определенных операторов.)

Он также должен быть легко компонуемым для таких выражений, как sin(A .+ cos(B[:,1])) .

Можно ли разобрать f1.(x, f2.(y .+ z)) как broadcast((a, b, c)->(f1(a, f2(b + c))), x, y, z) ?

Редактировать: я вижу, что это уже упоминалось выше... в комментарии, скрытом по умолчанию @github..

@yuyichao , слияние циклов кажется возможным, если функции помечены как @pure (по крайней мере, если элтипы неизменяемы), как я прокомментировал в #15032, но это задача компилятора, а не парсер. (Но такой векторизованный синтаксис больше для удобства, чем для выдавливания последнего цикла из критических внутренних циклов.)

Помните, что ключевой целью здесь является устранение необходимости в функциях @vectorized ; для этого требуется синтаксис, по крайней мере, такой же общий, почти такой же удобный и, по крайней мере, такой же быстрый. Он не требует автоматического слияния циклов, хотя было бы неплохо сообщить компилятору о намерении пользователя broadcast открыть возможность слияния циклов в будущем.

Есть ли какой-либо недостаток, если он также выполняет слияние циклов?

@yuyichao , слияние циклов - гораздо более сложная проблема, и это не всегда возможно, даже если отбросить нечистые функции (например, см. Пример exp(log(A) .- sum(A,1)) @StefanKarpinski выше). Промедление с реализацией этого, вероятно, приведет к тому, что оно _никогда_ не будет реализовано, на мой взгляд — мы должны делать это постепенно. Начните с раскрытия намерения пользователя. Если мы сможем оптимизировать в будущем, отлично. Если нет, у нас все еще есть обобщенная замена горстки «векторизованных» функций, доступных сейчас.

Другим препятствием является то, что .+ и т. д. в настоящее время не доступны синтаксическому анализатору как операция broadcast ; .+ — это просто еще одна функция. Мой план состоит в том, чтобы изменить это (сделать .+ сахара за broadcast(+, ...) ), как указано выше. Но опять же, гораздо легче добиться прогресса, если изменения будут постепенными.

Я имею в виду, что выполнить слияние циклов, доказав, что это действительно так, сложно, поэтому мы можем позволить синтаксическому анализатору выполнить преобразование как часть схемы. В приведенном выше примере это можно записать как. exp.(log.(A) .- sum(A,1)) и анализироваться как broadcast((x, y)->exp(log(x) - y), A, sum(A, 1)) .

Также хорошо, если .+ еще не принадлежит к той же категории (точно так же, как любой вызов функции, не транслируемой на доску, будет помещен в аргумент), и даже хорошо, если мы будем делать это (слияние цикла) только в более поздняя версия. В основном я спрашиваю, возможно ли наличие такой схемы (т.е. недвусмысленной) в синтаксическом анализаторе, и есть ли какой-либо недостаток, позволяя таким образом писать векторизованный и плавкий цикл.

выполнять слияние циклов, доказывая, что это действительно так, сложно

Я имею в виду, что сделать это в компиляторе сложно (может быть, и возможно), особенно потому, что компилятору необходимо изучить сложную реализацию broadcast , если только мы не используем специальный случай broadcast в компиляторе, который вероятно, плохая идея, и мы должны избегать этого, если это возможно...

Может быть? Это интересная идея, и не кажется невозможным определить синтаксис .( как «слияние» таким образом и оставить на усмотрение вызывающей стороны не использовать его для нечистых функций. Лучше всего было бы попробовать и посмотреть, нет ли тяжелых случаев (явных проблем я сейчас не наблюдаю), но я склоняюсь к этому после "нефьюзингового" PR.

Я склоняюсь к этому после "нефьюзийного" пиара.

Полностью согласен, тем более что .+ все равно не обрабатывается.

Я не хочу срывать это, но предложение @yuyichao натолкнуло меня на некоторые идеи. Акцент здесь делается на том, какие функции векторизуются, но мне это всегда кажется немного неуместным — реальный вопрос заключается в том, какие переменные векторизовать, что полностью определяет форму результата. Вот почему я был склонен помечать аргументы для векторизации, а не помечать функции для векторизации. Маркировка аргументов также позволяет использовать функции, которые векторизуются по одному аргументу, но не по другому. Тем не менее, мы можем иметь и то, и другое, и этот PR служит непосредственной цели замены встроенных векторизованных функций.

@StefanKarpinski , когда вы вызываете f.(args...) или broadcast(f, args...) , он векторизует _все_ аргументы. (Для этой цели вспомните, что скаляры обрабатываются как 0-мерные массивы.) В предложении @yuyichao f.(args...) = _fused широковещательный синтаксис_ (который мне нравится все больше и больше), я думаю, что слияние будет " stop" на любом выражении, отличном от func.(args...) (чтобы включить .+ и т. д. в будущем).

Так, например, sin.(x .+ cos.(x .^ sum(x.^2))) превратится (в julia-syntax.scm ) в broadcast((x, _s_) -> sin(x + cos(x^_s_)), x, sum(broacast(^, x, 2))) . Обратите внимание, что функция sum будет «границей слияния». Вызывающий будет нести ответственность за то, чтобы не использовать f.(args...) в тех случаях, когда слияние испортит побочные эффекты.

У вас есть пример, когда этого было бы недостаточно?

что мне нравится все больше и больше

Я рада что тебе нравится. знак равно

Просто еще одно расширение, которое, вероятно, не принадлежит к тому же раунду, возможно, можно использовать .= , .*= или подобное для решения проблемы назначения на месте (путем его отличия от нормальное задание)

Да, отсутствие слияния для других операций было моим главным возражением против .+= и так далее в #7052, но я думаю, что это будет решено путем объединения .= с другими вызовами func.(args...) . Или просто предохраните x[:] = ... .

:thumbsup: В этом обсуждении слились две концепции, которые на самом деле совершенно ортогональны:
матлабы "плавные широковещательные операции" или x .* y .+ z и апл'ы "карты на товары и зипы" типа f[product(I,J)...] и f[zip(I,J)...] . Разговоры друг с другом могут быть связаны с этим.

@mschauer , f.(I, J) уже (в #15032) эквивалентно map(x -> f(x...), zip(I, J) , если I и J имеют одинаковую форму. И если I — вектор-строка, а J — вектор-столбец, или наоборот, тогда broadcast действительно отображает набор произведений (или вы можете сделать f.(I, J') , если они оба являются массивами 1d). Так что я не понимаю, почему вы считаете, что эти понятия «вполне ортогональны».

Ортогональные — не то слово, они просто достаточно разные, чтобы сосуществовать.

Однако дело в том, что нам не нужны отдельные синтаксисы для этих двух случаев. func.(args...) может поддерживать оба варианта.

Как только член триумвирата (Стефан, Джефф, Вирал) объединит #15032 (который, я думаю, готов к слиянию), я закрою это и подам дорожную карту, чтобы обрисовать в общих чертах оставшиеся предлагаемые изменения: исправить вычисление типа вещания, объявить устаревшим @vectorize , превратить .op в широковещательный сахар, добавить «широковещательное слияние» на уровне синтаксиса и, наконец, объединить с назначением на месте. Последние два, вероятно, не войдут в 0.5.

Эй, я очень счастлив и благодарен за 15032. Я бы не стал пренебрегать обсуждением. Например, векторы векторов и подобные объекты по-прежнему очень неудобно использовать в julia, но они могут прорастать, как сорняки, в результате осмысления. Хорошая неявная нотация, не основанная на кодировании итерации в одноэлементные измерения, может значительно упростить эту задачу, например, с помощью расширенных итераторов и новых выражений генератора.

Я думаю, что теперь это может быть закрыто в пользу # 16285.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги