Julia: Sintaxe alternativa para `map(func, x)`

Criado em 23 set. 2014  ·  283Comentários  ·  Fonte: JuliaLang/julia

Isso foi discutido em alguns detalhes aqui . Eu estava tendo problemas para encontrá-lo, e pensei que merecia seu próprio problema.

breaking speculative

Comentários muito úteis

Assim que um membro do triunvirato (Stefan, Jeff, Viral) mesclar #15032 (que acho que está pronto para mesclagem), fecharei isso e enviarei um problema de roteiro para descrever as alterações propostas restantes: corrigir computação de tipo de transmissão, descontinuar @vectorize , transformar .op em açúcar de transmissão, adicionar "fusão de transmissão" no nível de sintaxe e, finalmente, fundir com atribuição no local. Os dois últimos provavelmente não chegarão ao 0,5.

Todos 283 comentários

+1

Ou func.(args...) como açúcar sintático para

broadcast(func, args...)

Mas talvez eu seja o único que preferiria isso?
De qualquer forma, +1.

:-1: Se alguma coisa, eu acho que a outra sugestão de Stefan de f[...] tem uma boa semelhança com compreensão.

Assim como @ihnorton , também não gosto muito dessa ideia. Em particular, não gosto da assimetria de ter a .+ b e sin.(a) .

Talvez não precisemos de sintaxe especial. Com #1470, poderíamos fazer algo como

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

certo? Talvez isso seja muito mágico, para obter o mapa automático em qualquer função.

@quinnj Essa linha resume meus maiores medos sobre permitir a sobrecarga de chamadas. Não vou conseguir dormir por dias.

Ainda não tenho certeza se é sintaticamente possível, mas e .sin(x) ? Isso é mais parecido com a .+ b ?

Acho que [] está ficando muito sobrecarregado e não funcionará para esse propósito. Por exemplo, provavelmente poderemos escrever Int(x) , mas Int[x] constrói uma matriz e, portanto, não pode significar map .

Eu estaria a bordo com .sin(x) .

Teríamos que recuperar alguma sintaxe para isso, mas se Int(x) for a versão escalar, então Int[x] é razoável por analogia para construir um vetor do tipo de elemento Int . Na minha opinião, a oportunidade de tornar essa sintaxe mais coerente é, na verdade, um dos aspectos mais atraentes da proposta f[v] .

Como fazer a sintaxe f[v] para map torna a sintaxe mais coerente? Não entendo. map tem uma "forma" diferente da sintaxe atual do construtor de matriz T[...] . Que tal Vector{Int}[...] ? Isso não funcionaria?

lol, desculpe o susto @JeffBezanson! Haha, a sobrecarga de chamadas é definitivamente um pouco assustadora, de vez em quando, penso nos tipos de ofuscação de código que você pode fazer em julia e com call , você pode fazer algumas coisas complicadas.

Acho que .sin(x) parece uma boa ideia também. Houve consenso sobre o que fazer com multi-args?

:-1:. Salvando alguns caracteres em comparação com o uso de funções de ordem superior, não acho que valha o custo em legibilidade. Você consegue imaginar um arquivo com .func() / func.() e func() intercalados em todos os lugares?

Parece provável que removeremos a sintaxe a.(b) de qualquer maneira, pelo menos.

Uau, fale sobre agitar um ninho de abelhas! Mudei o nome para refletir melhor a discussão.

Também podemos renomear map de 2 argumentos para zipWith :)

Se alguma sintaxe for realmente necessária, que tal [f <- b] ou outro trocadilho com compreensão _dentro_ dos colchetes?

( @JeffBezanson você está com medo de que alguém escreva CJOS ou Moose.jl :) ... se conseguirmos esse recurso, basta colocá-lo na seção Don't do stupid stuff: I won't optimize that do manual)

Atualmente, escrever Int[...] indica que você está construindo uma matriz do tipo de elemento Int . Mas se Int(x) significa converter x em Int aplicando Int como uma função, então você também pode considerar Int[...] "aplicar Int para cada coisa em ...", ah que por sinal produz valores do tipo Int . Assim, escrever Int[v] seria equivalente a [ Int(x) for x in v ] e Int[ f(x) for x in v ] seria equivalente a [ Int(f(x)) for x in v ] . Claro, então você perdeu um pouco da utilidade de escrever Int[ f(x) for x in v ] em primeiro lugar – ou seja, que podemos saber estaticamente que o tipo de elemento é Int – mas se impor que Int(x) deve produzir um valor do tipo Int (não uma restrição irracional), então poderíamos recuperar essa propriedade.

Parece-me mais vetorização/inferno de gato implícito. O que Int[x, y] faria? Ou pior, Vector{Int}[x] ?

Não estou dizendo que é a melhor ideia de todos os tempos ou mesmo defendendo-a – estou apenas apontando que ela não colide _completamente_ com o uso existente, que é em si um pouco um hack. Se pudéssemos tornar o uso existente parte de um padrão mais coerente, seria uma vitória. Não tenho certeza do que f[v,w] significaria – as escolhas óbvias são [ f(x,y) for x in v, y in w ] ou map(f,v,w) mas ainda há mais opções.

Eu sinto que a.(b) é pouco usado. Executou um teste rápido e é usado apenas em 54 dos ~ 4.000 arquivos de origem julia em estado selvagem: https://gist.github.com/jakebolewski/104458397f2e97a3d57d.

Acho que se choca completamente. T[x] tem a "forma" T --> Array{T} , enquanto map tem a forma Array{T} --> Array{S} . Esses são praticamente incompatíveis.

Para fazer isso, acho que teríamos que desistir de T[x,y,z] como construtor por Vector{T} . Indexação de array simples, A[I] onde I é um vetor, pode ser visto como map(i->A[i], I) . "Aplicar" um array é como aplicar uma função (é claro que o matlab ainda usa a mesma sintaxe para eles). Nesse sentido, a sintaxe realmente funciona, mas perderíamos a sintaxe de vetor digitado no processo.

Eu meio que sinto que debater sintaxe aqui distrai da mudança mais importante: fazer map rápido.

Obviamente, tornar map rápido (o que, aliás, precisa ser parte de um redesenho bastante completo da noção de funções em julia) é mais importante. No entanto, passar de sin(x) para map(sin, x) é muito significativo do ponto de vista da usabilidade, portanto, para realmente matar a vetorização, a sintaxe é muito importante.

No entanto, passar de sin(x) para map(sin, x) é muito significativo do ponto de vista da usabilidade, então para realmente matar a vetorização, a sintaxe é bastante importante.

Totalmente acordado.

Eu concordo com @JeffBezanson que f[x] é praticamente irreconciliável com as atuais construções de matrizes digitadas Int[x, y] etc.

Outra razão para preferir .sin ao invés sin. é finalmente permitir usar, por exemplo Base.(+) para acessar a função + em Base (uma vez a.(b) é removido).

Quando um módulo define sua própria função sin (ou qualquer outra) e queremos usar essa função em um vetor, fazemos Module..sin(v) ? Module.(.sin(v)) ? Module.(.sin)(v) ? .Module.sin(v) ?

Nenhuma dessas opções realmente parece mais boa.

Sinto que esta discussão perde a essência do problema. Ou seja: ao mapear funções de argumento único para contêineres, sinto que a sintaxe map(func, container) é _already_ clara e sucinta. Em vez disso, é apenas ao lidar com vários argumentos que sinto que podemos nos beneficiar de uma sintaxe melhor para curry.

Tome por exemplo a verbosidade de map(x->func(x,other,args), container) , ou encadeie uma operação de filtro para torná-la pior filter(x->func2(x[1]) == val, map(x->func1(x,other,args), container)) .

Nesses casos, sinto que uma sintaxe de mapa reduzida não ajudaria muito. Não que eu ache que isso seja particularmente ruim, mas a) eu não acho que um short-hand map ajudaria muito eb) eu adoro ficar atrás da sintaxe de Haskell. ;)

IIRC, em Haskell o acima pode ser escrito filter ((==val) . func2 . fst) $ map (func1 other args) container com uma ligeira mudança na ordem dos argumentos para func1 .

Em elm .func é definido por x->x.func e isso é muito útil, veja elm records . Isso deve ser considerado antes de usar essa sintaxe para map .

Eu gosto disso.

Embora o acesso de campo não seja tão importante em Julia como em muitos idiomas.

Sim, parece menos relevante aqui, pois os campos em Julia são mais para uso "privado". Mas com a discussão em andamento sobre sobrecarregar o acesso ao campo, isso pode se tornar mais sensato.

f.(x) parece a solução menos problemática, se não fosse pela assimetria com .+ . Mas manter a associação simbólica de . para "operação de elemento sábio" é uma boa ideia IMHO.

Se a construção atual do array tipado pode ser obsoleta, então func[v...] pode ser convertido para map(func, v...) , e os arrays literais podem ser escritos T[[a1, ..., an]] (em vez do atual T[a1, ..., an] ).

Acho sin∘v tranquilo natural também (quando uma matriz v é vista como uma aplicação de índices para valores contidos), ou mais simplesmente sin*v ou v*[sin]' ( que requer a definição *(x, f::Callable) ) etc.

Voltando a esta questão com a mente fresca, percebi que f.(x) pode ser visto como uma sintaxe bastante natural. Em vez de lê-lo como f. e ( , você pode lê-lo como f e .( . .( é então metaforicamente uma versão elementar do operador de chamada de função ( , que é totalmente consistente com .+ e amigos.

A ideia de .( ser um operador de chamada de função me deixa muito triste.

@johnmyleswhite Cuidados para elaborar? Eu estava falando sobre a intuitividade da sintaxe, ou sua consistência visual com o resto da linguagem, não sobre a implementação técnica.

Para mim, ( não faz parte da semântica da linguagem: é apenas parte da sintaxe. Então eu não gostaria de ter que inventar uma maneira para .( e ( começarem a ser diferentes. O primeiro gera um multicall Expr em vez de um call Expr ?

Não. Como eu disse, não estava insinuando que deveria haver dois operadores de chamadas diferentes. Apenas tentando encontrar uma sintaxe _visualmente_ consistente para operações elementares.

Para mim, o que mata essas opções é a questão de como vetorizar funções com vários argumentos. Não há uma única maneira de fazer isso e qualquer coisa que seja geral o suficiente para suportar todas as formas possíveis começa a se parecer muito com as compreensões de array multidimensional que já temos.

É bastante padrão para multi-argumento map iterar sobre todos os argumentos.
Se fizéssemos isso, eu faria .( sintaxe para uma chamada para mapear. Essa sintaxe pode
não ser tão bom por vários motivos, mas eu ficaria bem com esses aspectos.

O fato de que várias generalizações são possíveis para funções de múltiplos argumentos não pode ser um argumento contra o suporte de pelo menos alguns casos especiais - assim como a transposição de matrizes é útil mesmo que possa ser generalizada de várias maneiras para tensores.

Só precisamos escolher a solução mais útil. As possíveis escolhas já foram discutidas aqui: https://github.com/JuliaLang/julia/issues/8389#issuecomment -55953120 (e comentários seguintes). Como @JeffBezanson disse que o comportamento atual de map é razoável. Um critério interessante é poder substituir @vectorize_2arg .

Meu ponto é que ter sin.(x) e x .+ y coexistindo é estranho. Prefiro ter .sin(x) -> map(sin, x) e x .+ y -> map(+, x, y) .

.+ na verdade usa broadcast .

Algumas outras ideias, por puro desespero:

  1. Sobrecarregar dois pontos, sin:x . Não generaliza bem para vários argumentos.
  2. sin.[x] --- esta sintaxe está disponível, atualmente sem sentido.
  3. sin@x --- não disponível, mas talvez possível

Eu realmente não estou convencido de que precisamos disso.

Nem eu. Acho que f.(x) é a melhor opção aqui, mas não gosto.

Mas sem isso, como podemos evitar criar todos os tipos de funções vetorizadas, em particular coisas como int() ? Foi isso que me levou a iniciar esta discussão em https://github.com/JuliaLang/julia/issues/8389.

Devemos encorajar as pessoas a usar map(func, x) . Não é muita digitação e fica imediatamente claro para qualquer um vindo de outro idioma.

E, claro, certifique-se de que é rápido.

Sim, mas para uso interativo acho muito doloroso. Isso vai ser um grande aborrecimento para as pessoas que vêm de R (pelo menos, eu não sei sobre outras linguagens), e eu não gostaria que isso passasse a sensação de que Julia não é adequada para análise de dados.

Outro problema é a consistência: a menos que você queira remover todas as funções vetorizadas atualmente, incluindo log , exp , etc., e peça às pessoas que usem map (que pode ser um teste interessante da praticidade dessa decisão), a linguagem será inconsistente, dificultando saber antecipadamente se uma função é vetorizada ou não (e em quais argumentos).

Muitas outras linguagens usam map há anos.

Como entendi o plano de parar de vetorizar tudo, remover a maioria/todas as funções vetorizadas sempre foi parte da estratégia.

Sim, claro que pararíamos de vetorizar tudo. A inconsistência já está aí: já é difícil saber quais funções são ou devem ser vetorizadas, já que não há uma razão realmente convincente para que sin , exp etc. devam ser mapeados implicitamente sobre arrays.

E dizer aos escritores da biblioteca para colocar @vectorize em todas as funções apropriadas é bobagem; você deve ser capaz de escrever apenas uma função, e se alguém quiser calculá-la para cada elemento, use map .

Vamos imaginar o que acontece se removermos as funções matemáticas vetorizadas comumente usadas:

  1. Pessoalmente, não me importo de escrever map(exp, x) em vez de exp(x) , embora o último seja um pouco mais curto e mais limpo. No entanto, existe uma _grande_ diferença no desempenho. A função vetorizada é cerca de 5x mais rápida que o mapa na minha máquina.
  2. Quando você está trabalhando com expressões compostas, o problema é mais interessante. Considere uma expressão composta: exp(0.5 * abs2(x - y)) , então temos
# 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 

Se formos remover funções matemáticas vetorizadas, a única maneira aceitável em legibilidade e desempenho parece ser a compreensão de matriz. Ainda assim, eles não são tão convenientes quanto a matemática vetorizada.

-1 para remover versões vetorizadas. Na verdade, bibliotecas como VML e Yeppp oferecem um desempenho muito maior para versões vetorizadas e precisamos descobrir como aproveitá-las.

Se estes estão na base ou não é uma discussão diferente e uma discussão maior, mas a necessidade é real e o desempenho pode ser maior do que o que temos.

@lindahua e @ViralBShah : Algumas de suas preocupações parecem se basear na suposição de que nos livraríamos de funções vetorizadas antes de fazermos melhorias em map , mas não acredito que alguém tenha proposto fazer isso.

Acho que o exemplo de @lindahua é bastante revelador: a sintaxe vetorizada é muito mais agradável e muito mais próxima da fórmula matemática do que as outras soluções. Seria muito ruim perder isso, e as pessoas vindas de outras linguagens científicas provavelmente vão considerar isso como um ponto negativo em Julia.

Eu estou bem em remover todas as funções vetorizadas (quando map é rápido o suficiente) e ver como isso acontece. Acredito que o interesse de fornecer uma sintaxe de conveniência será ainda mais visível nesse ponto, e ainda será tempo de adicioná-la se for o caso.

Acho que Julia é diferente de muitas outras linguagens porque enfatiza o uso interativo (expressões mais longas são irritantes de digitar nesse caso) e cálculos matemáticos (as fórmulas devem estar o mais próximo possível das expressões matemáticas para tornar o código legível). É em parte por isso que Matlab, R e Numpy oferecem funções vetorizadas (a outra razão é, claro, o desempenho, um problema que pode desaparecer em Julia).

Minha impressão da discussão é que o significado da matemática vetorizada é subestimado. Na verdade, uma das principais vantagens da matemática vetorizada está na concisão da expressão - é muito mais do que apenas um paliativo para "linguagens com loop for lento".

Comparando y = exp(x) e

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

O primeiro é obviamente muito mais conciso e legível do que o último. O fato de Julia tornar os loops eficientes não significa que devemos sempre desvetorizar os códigos, o que, para mim, é bastante contraproducente.

Devemos encorajar as pessoas a escreverem códigos de forma natural. Por um lado, isso significa que não devemos tentar escrever códigos complicados e distorcer funções vetorizadas em um contexto no qual eles não se encaixam; por outro lado, devemos apoiar o uso de matemática vetorizada sempre que fizer mais sentido.

Na prática, mapear fórmulas para matrizes de números é uma operação muito comum, e devemos nos esforçar para tornar isso conveniente em vez de complicado. Para isso, os códigos vetorizados continuam sendo a forma mais natural e concisa. Além do desempenho, eles ainda são melhores do que chamar a função map , especialmente para expressões compostas com vários argumentos.

Certamente não queremos versões vetorizadas de tudo, mas usar map toda vez para vetorizar seria irritante pelas razões que Dahua mencionou acima.

Se map fosse rápido, certamente nos permitiria focar em ter um conjunto menor e significativo de funções vetorizadas.

Devo dizer que estou fortemente do lado de apoiar uma notação de mapa concisa. Sinto que é o melhor compromisso entre as diferentes necessidades.

Eu não gosto de funções vetorizadas. Ele esconde o que está acontecendo. Esse hábito de criar versões vetorizadas de funções leva a códigos misteriosos. Digamos que você tenha algum código onde uma função f de um pacote é chamada em um vetor. Mesmo que você tenha alguma ideia do que a função faz, não pode ter certeza ao ler o código se ela faz isso de forma elementar ou funciona no vetor como um todo. Se as linguagens de computação científica não tivessem um histórico de ter essas funções vetorizadas, não acho que as aceitaríamos agora.

Isso também leva à situação em que você é meio que implicitamente encorajado a escrever versões vetorizadas de funções para habilitar um código conciso onde as funções são usadas.

O código mais explícito sobre o que está acontecendo é o loop, mas como diz @lindahua acaba sendo muito verboso, o que tem suas próprias desvantagens, principalmente em uma linguagem que também serve para uso interativo.

Isso leva ao compromisso map , que sinto que está mais próximo do ideal, mas ainda concordo com @lindahua que não é conciso o suficiente.

Onde vou discordar da @lindahua é que as funções vetorizadas são a melhor escolha, pelos motivos que mencionei anteriormente. O que meu raciocínio leva é que Julia deveria ter uma notação muito concisa para map .

Acho realmente atraente como o Mathematica faz isso com sua notação abreviada. A notação abreviada para aplicar uma função a um argumento no Mathematica é @ , então você usaria Apply a função f para um vetor como: f @ vector . A notação abreviada relacionada para mapear uma função é /@ , então você mapeia f para o vetor como: f /@ vector . Isso tem várias características atraentes. Ambas as mãos curtas são concisas. O fato de ambos usarem o símbolo @ enfatiza que há uma relação entre o que eles fazem, mas o / no mapa ainda o torna visualmente distinto para deixar claro quando você está mapeando e quando você não são. Isso não quer dizer que Julia deva copiar cegamente a notação do Mathematica, apenas que uma boa notação para mapeamento é incrivelmente valiosa

Não estou sugerindo livrar-se de todas as funções vetorizadas. Aquele trem já deixou a estação há muito tempo. Em vez disso, sugiro manter a lista de funções vetorizadas o mais curta possível e fornecer uma boa notação de mapa conciso para desencorajar a adição à lista de funções vetorizadas.

Tudo isso, é claro, condicionado a map e funções anônimas serem rápidas. Agora Julia está em uma posição estranha. Costumava ser o caso em linguagens de computação científica que as funções eram vetorizadas porque os loops são lentos. Isso não é um problema. Em vez disso, em Julia, você vetoriza funções porque funções mapeadas e anônimas são lentas. Então, estamos de volta onde começamos, mas por razões diferentes.

As funções de biblioteca vetorizadas têm uma desvantagem -- apenas as funções explicitamente fornecidas pela biblioteca estão disponíveis. Ou seja, por exemplo, sin(x) é rápido quando aplicado a um vetor, enquanto sin(2*x) é subitamente muito mais lento, pois requer um array intermediário que precisa ser percorrido duas vezes (primeiro escrevendo, depois lendo).

Uma solução seria uma biblioteca de funções matemáticas vectorizABLE. Estas seriam implementações de sin , cos , etc. que estão disponíveis para LLVM para inlining. O LLVM poderia então vetorizar esse loop e, esperançosamente, levar a um código muito eficiente. Yeppp parece ter os kernels de loop corretos, mas não parece expô-los para inlining.

Outro problema com a vetorização como paradigma é que ela simplesmente não funciona se você usar tipos de contêiner diferentes dos abençoados pela biblioteca padrão. Você pode ver isso em DataArrays: há uma quantidade ridícula de código de metaprogramação usado para revetorizar funções para os novos tipos de contêiner que estamos definindo.

Combine isso com o ponto de @eschnett e você terá:

  • As funções vetorizadas só funcionam se você se restringir às funções da biblioteca padrão
  • As funções vetorizadas só funcionam se você se restringir aos tipos de contêiner na biblioteca padrão

Quero esclarecer que meu ponto não é que devemos sempre manter as funções vetorizadas, mas que precisamos de uma maneira tão concisa quanto escrever funções vetorizadas. Usar map provavelmente não satisfaz isso.

Eu gosto da ideia do @eschnett de marcar algumas funções como _vectorizable_, e o compilador pode mapear automaticamente uma função vetorizável para um array sem exigir que os usuários definam explicitamente uma versão vetorizada. O compilador também pode fundir uma cadeia de funções vetorizáveis ​​em um loop fundido.

Aqui está o que tenho em mente, inspirado nos comentários de @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

O compilador também pode revetorizar a computação (em nível inferior) identificando a oportunidade de usar o SIMD.

Claro, @vec deve ser disponibilizado para o usuário final, para que as pessoas possam declarar suas próprias funções como vetorizáveis.

Obrigado, @lindahua : seu esclarecimento ajuda muito.

@vec seria diferente de declarar uma função @pure ?

@vec indica que a função pode ser mapeada de maneira elementar.

Nem todas as funções puras se enquadram nesta categoria, por exemplo, sum é uma função pura, e não acho aconselhável declará-la como _vectorizable_.

Não foi possível recuperar a propriedade vec de sum de pure e associative tags em + juntamente com o conhecimento sobre como reduce / foldl / foldr funcionam quando recebem as funções pure e associative ? Obviamente, tudo isso é hipotético, mas se Julia se concentrasse em traços para tipos, eu poderia imaginar melhorar substancialmente o estado da arte para vetorização, também investindo em traços para funções.

Eu sinto que adicionar uma nova sintaxe é o oposto do que queremos (depois de apenas limpar a sintaxe especial para Any[] e Dict). Todo o _point_ de remover essas funções vetorizadas é reduzir casos especiais (e não acho que a sintaxe deva ser diferente da semântica da função). Mas concordo que um mapa conciso seria útil.

Então, por que não adicionar um operador de infixo conciso map ? Aqui eu vou pegar $ arbitrariamente. Isso faria com que o exemplo de @lindahua passasse de

exp(0.5 * abs2(x - y))

para

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

Agora, se tivéssemos apenas suporte semelhante ao Haskell para operadores infixos definidos pelo usuário, isso seria apenas uma alteração de uma linha ($) = map . :)

IMO, as outras propostas de sintaxe são visualmente muito próximas da sintaxe existente e exigiriam mais esforço mental para analisar ao examinar o código:

  • foo.(x) -- visualmente semelhante ao acesso de membro do tipo padrão
  • foo[x] -- estou acessando o x-ésimo membro da matriz foo ou chamando o mapa aqui?
  • .foo(x) -- tem problemas como @kmsquire apontou

E eu sinto que a solução @vec está muito próxima do @vectorize que estamos tentando evitar em primeiro lugar. Certamente, algumas anotações ala #8297 seriam boas de se ter e poderiam ajudar um compilador futuro e mais inteligente a reconhecer essas oportunidades de fusão de fluxo e otimizar de acordo. Mas não gosto da ideia de forçar.

O mapa Infix mais funções anônimas rápidas também podem ajudar na criação de temporários se permitirem que você faça algo como:

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

Eu me pergunto se a ideia do novo e legal Trait.jl pode ser emprestada no contexto de designar uma função sendo vetorizável. Claro, neste caso estamos olhando para _instâncias_ individuais do tipo Function sendo vetorizáveis ​​ou não, ao invés de um tipo julia ter um traço específico.

Agora, se tivéssemos suporte semelhante ao Haskell para operadores infixos definidos pelo usuário

6582 #6929 não é suficiente?

Há um ponto nesta discussão sobre vetorizar expressões inteiras com o menor número possível de temporários de array. Usuários que querem sintaxe vetorizada não vão querer apenas um exp(x) vetorizado; eles gostariam de escrever expressões como

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

e tê-lo magicamente vetorizado

Não seria necessário marcar funções como "vetorizáveis". Esta é mais uma propriedade de como as funções são implementadas e disponibilizadas para Julia. Se eles forem implementados, por exemplo, em C, eles precisam ser compilados para bytecode LLVM (não arquivos de objeto) para que o otimizador LLVM ainda possa acessá-los. Implementá-los em Julia também funcionaria.

Vetorização significa que se implementa a função de uma maneira que é muito bem descrita pelo projeto Yeppp: sem ramificações, sem tabelas, divisão ou raiz quadrada se estiverem disponíveis como instruções vetoriais em hardware e, caso contrário, muitas operações de multiplicação e adição fundidas e vetoriais operações de mesclagem.

Infelizmente, tais implementações serão dependentes de hardware, ou seja, pode-se ter que escolher diferentes algoritmos ou diferentes implementações dependendo de quais instruções de hardware são eficientes. Eu fiz isso no passado (https://bitbucket.org/eschnett/vecmathlib/wiki/Home) em C++ e com um público-alvo ligeiramente diferente (operações baseadas em estêncil que são vetorizadas manualmente em vez de uma vetorização automática compilador).

Aqui em Julia, as coisas seriam mais fáceis já que (a) sabemos que o compilador será LLVM, e (b) podemos implementar isso em Julia em vez de C++ (macros vs. templates).

Há uma outra coisa a considerar: se alguém desistir de partes do padrão IEEE, poderá melhorar muito a velocidade. Muitos usuários sabem que, por exemplo, números desnormalizados não são importantes, ou que a entrada sempre será menor que sqrt(max(Double)) , etc. A questão é se oferecer caminhos rápidos para esses casos. Eu sei que estarei muito interessado nisso, mas outros podem preferir resultados exatamente reprodutíveis.

Deixe-me criar um protótipo exp vetorizável em Julia. Podemos então ver como o LLVM faz a vetorização de um loop e quais velocidades obtemos.

É muito assustador usar parênteses de largura total em torno do argumento da função ~

Desculpe, eu não percebi que estava repetindo exatamente a mesma coisa que @johnmyleswhite estava falando acima de função com traço. Continuar.

@eschnett Não acho razoável vincular a API (se as funções são vetorizáveis ​​ou não) aos detalhes da implementação (como a função é compilada). Parece bastante complexo entender e manter estável no tempo e entre arquiteturas, e não funcionaria ao chamar funções em bibliotecas externas, por exemplo, log não seria detectado como vetorizável, pois chama uma função do openlibm.

A ideia de OTOH @johnmyleswhite de usar traços para comunicar quais são as propriedades matemáticas de uma função pode ser uma ótima solução. (A proposta da @lindahua é um recurso que sugeri em algum lugar há algum tempo, mas a solução de usar traits pode ser ainda melhor.)

Agora, se tivéssemos suporte semelhante ao Haskell para operadores infixos definidos pelo usuário

6582 #6929 não é suficiente?

Eu deveria ter dito: ... operadores infixos _não-unicode_ definidos pelo usuário, pois não acho que queremos exigir que os usuários digitem caracteres unicode para acessar essa funcionalidade principal. Embora eu veja que $ é realmente um dos adicionados, então obrigado por isso! Uau, então isso realmente funciona em Julia _hoje_ (mesmo que não seja "rápido" ... ainda):

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

Não sei se é a melhor escolha por map mas usar $ por xor realmente parece um desperdício. O xor bit a bit não é usado com tanta frequência. map é muito mais importante.

O ponto de @jiahao acima é muito bom: funções vetorizadas individuais como exp são na verdade uma espécie de hack para obter _expressions_ vetorizadas como exp(-x^2) . A sintaxe que faz algo como @devec seria realmente valiosa: você obteria desempenho devectorizado mais a generalidade de não precisar identificar funções individualmente como vetorizadas.

A capacidade de usar traços de função para isso seria legal, mas ainda acho menos satisfatório. O que realmente está acontecendo em geral é que uma pessoa escreve uma função e outra a repete.

Concordo que isso não é uma propriedade da função, é uma propriedade do uso da função. A discussão sobre a aplicação de traços parece um caso de latir para a árvore errada.

Brainstorming: que tal você marcar os argumentos que deseja mapear para que ele suporte o mapeamento multi-args:

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"""

Não tenho certeza se .b ou b. é melhor para mostrar que você deseja mapear. Eu gosto de retornar um resultado 3x2 multidimensional neste caso, pois representa a forma do ping map .

Glen

Aqui https://github.com/eschnett/Vecmathlib.jl é um repositório com uma amostra
implementação de exp , escrito de uma forma que pode ser otimizada pelo LLVM.
Esta implementação é cerca de duas vezes mais rápida que o padrão exp
implementação no meu sistema. (provavelmente) ainda não atinge a velocidade de Yeppp,
provavelmente porque o LLVM não desenrola o respectivo loop SIMD como
agressivamente como Yeppp. (Eu comparei as instruções desmontadas.)

Escrever uma função exp vetorizável não é fácil. Usando fica assim:

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

onde o loop j e os argumentos da função estão lá apenas para
propósitos de benchmarking.

Existe uma macro @unroll para Julia?

-erik

Em domingo, 2 de novembro de 2014 às 20h26, Tim Holy [email protected] escreveu:

Eu concordo que isso não é uma propriedade da função, é uma propriedade de
o uso da função. A discussão sobre a aplicação de traços parece um
caso de latir para a árvore errada.

Responda a este e-mail diretamente ou visualize-o no GitHub
https://github.com/JuliaLang/julia/issues/8450#issuecomment -61433026.

Erik Schnetter [email protected]
http://www.perimeterinstitute.ca/personal/eschnetter/

funções vetorizadas individuais como exp são na verdade uma espécie de hack para obter _expressions_ vetorizadas como exp(-x^2)

A sintaxe central para levantar expressões inteiras do domínio escalar seria muito interessante. A vetorização é apenas um exemplo (onde o domínio alvo são vetores); outro caso de uso interessante seria subir para o domínio da matriz (#5840) onde a semântica é bem diferente. No domínio da matriz também seria útil explorar como o dispatch em diferentes expressões poderia funcionar, pois no caso geral você desejaria Schur-Parlett e outros algoritmos mais especializados se quisesse algo mais simples como sqrtm . (E com uma sintaxe inteligente, você pode se livrar inteiramente das funções *m - expm , logm , sqrtm , ...)

Existe uma macro @unroll para Julia?

usando Base.Cartesiana
@nexpr 4 d->(y[i+d] = exp(x[i+d])

(Consulte http://docs.julialang.org/en/latest/devdocs/cartesian/ se tiver dúvidas.)

@jiahao Generalizar isso para funções de matriz parece um desafio interessante, mas meu conhecimento sobre isso é quase nulo. Você tem alguma ideia de como funcionaria? Como isso se articularia com a vetorização? Como a sintaxe permitiria fazer a diferença entre aplicar exp elementos em um vetor/matriz e calcular sua matriz exponencial?

@timholy : Obrigado! Não pensei em usar o cartesiano para desenrolar.

Infelizmente, o código produzido por @nexprs (ou por desenrolamento manual) não é mais vetorizado. (Este é o LLVM 3.3, talvez o LLVM 3.5 seja melhor.)

Re: desenrolando, veja também o post de @toivoh em julia-users . Também pode valer a pena dar uma chance ao #6271.

@nalimilan Ainda não pensei nisso, mas o levantamento escalar->matriz seria bastante simples de implementar com uma única função matrixfunc (digamos). Uma sintaxe hipotética (completamente inventando algo) poderia ser

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

que seria então

  1. identificar os domínios de origem e destino do aumento de X sendo do tipo Matrix{Float64} e tendo elementos (parâmetro de tipo) Float64 (definindo assim implicitamente um aumento de Float64 => Matrix{Float64} ) , então
  2. chame matrixfunc(x->exp(c*x^2)*sin(x), X) para calcular o equivalente a expm(c*X^2)*sinm(X) , mas evitando a multiplicação da matriz.

Em algum outro código X poderia ser Vector{Int} e o levantamento implícito seria de Int para Vector{Int} , e então lift(x->exp(c*x^2)*sin(x), X) poderia então ligue para map(x->exp(c*x^2)*sin(x), X) .

Pode-se imaginar também outros métodos que especificam explicitamente os domínios de origem e destino, por exemplo, lift(Number=>Matrix, x->exp(c*x^2)*sin(x), X) .

@nalimilan Vetorização não é realmente uma propriedade da API. Com a tecnologia de compilador de hoje, uma função só pode ser vetorizada se estiver em linha. As coisas dependem principalmente da implementação da função - se ela for escrita da "maneira correta", o compilador poderá vetorizá-la (depois de inline-la em um loop circundante).

@eschnett : Você está usando o mesmo significado de vetorização que os outros? Parece que você é sobre SIMD, etc., o que não é o que eu entendo que @nalimilan quer dizer.

Certo. Existem duas noções diferentes de vetorização aqui. Um ofertas
com a obtenção do SIMD para loops internos apertados (vetorização do processador). O principal
questão que está sendo discutida aqui é a sintaxe/semântica de alguma forma "automaticamente"
ser capaz de chamar uma função de argumento único (ou multi) em uma coleção.

Em terça-feira, 4 de novembro de 2014 às 19h04, John Myles White [email protected]
escrevi:

@eschnett https://github.com/eschnett : Você está usando o mesmo significado
de vetorização como outros? Parece que você está falando sobre SIMD etc., o que
não é o que eu entendo @nalimilan https://github.com/nalimilan para
quer dizer.


Responda a este e-mail diretamente ou visualize-o no GitHub
https://github.com/JuliaLang/julia/issues/8450#issuecomment -61738237.

Em simetria com outros operadores . , f.(x) não deveria aplicar uma coleção de funções a uma coleção de valores? (Por exemplo, para transformar de algum sistema de coordenadas de unidade nd para coordenadas físicas.)

Ao discutir a sintaxe, surgiu a noção de que usar loops explícitos para expressar o equivalente a map(log, x) era muito lento. Portanto, se alguém puder fazer isso rápido o suficiente, chamar map (ou usar uma sintaxe especial) ou escrever loops são equivalentes no nível semântico, e não é necessário introduzir uma desambiguação sintática. Atualmente, chamar uma função de log de vetor é muito mais rápido do que escrever um loop em uma matriz, levando as pessoas a pedir uma maneira de expressar essa distinção em seu código.

Existem dois níveis de problemas aqui: (1) sintaxe e semântica, (2) implementação.

A questão de sintaxe e semântica é sobre como o usuário pode expressar a intenção de mapear determinada computação de maneira elementar/transmissão para determinadas matrizes. Atualmente, Julia suporta duas maneiras: usando funções vetorizadas e permitindo que os usuários escrevam o loop explicitamente (às vezes com a ajuda de macros). Nenhuma das formas é ideal. Embora as funções vetorizadas permitam escrever expressões muito concisas como exp(0.5 * (x - y).^2) , elas têm dois problemas: (1) é difícil traçar uma linha sobre quais funções devem fornecer uma versão vetorizada e quais não, muitas vezes resultando em debates intermináveis ​​no lado do desenvolvedor e confusão no lado do usuário (muitas vezes você tem que procurar o documento para descobrir se certas funções são vetorizadas). (2) Torna difícil fundir os laços entre os limites da função. Neste ponto e provavelmente por vários meses/anos, o compilador provavelmente não será capaz de executar tarefas tão complexas como examinar várias funções juntas, identificar o fluxo de dados conjunto e produzir um caminho de código otimizado entre os limites da função.

Usar a função map resolve o problema (1) aqui. Isso, no entanto, ainda não fornece nenhuma ajuda para resolver o problema (2) - usar funções, seja uma função vetorizada específica ou um map genérico, sempre cria um limite de função que impede a fusão de loops, o que é crítico em computação de alto desempenho. Usar a função map também leva à verbosidade, por exemplo, a expressão acima agora se torna uma instrução mais longa como map(exp, 0.5 * map(abs2, x - y)) . Você pode razoavelmente imaginar que esse problema seria agravado com expressões mais complexas.

Entre todas as propostas descritas neste tópico, pessoalmente sinto que usar notações especiais para indicar mapeamento é o caminho mais promissor a seguir. Em primeiro lugar, mantém a concisão da expressão. Pegue a notação $ por exemplo, as expressões acima agora podem ser escritas como exp $(0.5 * abs2$(x - y)) . Isso é um pouco mais longo do que a expressão vetorizada original, mas não é tão ruim - o que tudo o que requer é inserir um $ para cada chamada de um mapeamento. Por outro lado, essa notação também serve como um indicador inequívoco de um mapeamento sendo executado, que o compilador pode utilizar para quebrar o limite da função e produzir um loop fundido. Neste curso, o compilador não precisa olhar para a implementação interna da função -- tudo o que ele precisa saber é que a função será mapeada para cada elemento dos arrays fornecidos.

Dadas todas as facilidades das CPUs modernas, especialmente a capacidade do SIMD, fundir vários loops em um é apenas um passo para a computação de alto desempenho. Esta etapa em si não aciona a utilização das instruções SIMD. A boa notícia é que agora temos a macro @simd . O compilador pode inserir essa macro no início do loop produzido quando achar que isso é seguro e benéfico.

Para resumir, acho que a notação $ (ou propostas semelhantes) pode resolver amplamente o problema de sintaxe e semântica, fornecendo as informações necessárias para o compilador fundir loops e explorar SIMD e, assim, emitir códigos de alto desempenho.

O resumo de @lindahua é um bom IMHO.

Mas acho que seria interessante estender isso ainda mais. Julia merece um sistema ambicioso que torne muitos padrões comuns tão eficientes quanto loops desenrolados.

  • O padrão de fusão de chamadas de funções aninhadas em um único loop também deve ser aplicado aos operadores, para que A .* B .+ C não leve à criação de dois temporários, mas apenas um para o resultado.
  • A combinação de funções elementares e reduções também deve ser tratada, para que a redução seja aplicada imediatamente após o cálculo do valor de cada elemento. Normalmente, isso permitirá livrar-se de sumabs2(A) , substituindo-o por uma notação padrão como sum(abs$(A)$^2) (ou sum(abs.(A).^2) ).
  • Finalmente, padrões de iteração não padrão devem ser suportados para matrizes não padrão, de modo que para matrizes esparsas A .* B só precise manipular entradas diferentes de zero e retorne uma matriz esparsa. Isso também seria útil se você deseja aplicar uma função de elemento a Set , a Dict ou até mesmo a Range .

Os dois últimos pontos podem funcionar fazendo com que as funções elementares retornem um tipo especial AbstractArray , digamos LazyArray , que calcularia seus elementos em tempo real (semelhante ao Transpose digite em https://github.com/JuliaLang/julia/issues/4774#issuecomment-59422003). Mas, em vez de acessar seus elementos ingenuamente, passando por cima deles usando índices lineares de 1 a length(A) , o protocolo iterador poderia ser usado. O iterador para um determinado tipo escolheria automaticamente se a iteração em linha ou em coluna é a mais eficiente, dependendo do layout de armazenamento do tipo. E para matrizes esparsas, permitiria pular zero entradas (o original e o resultado precisariam ter uma estrutura comum, cf. https://github.com/JuliaLang/julia/issues/7010, https://github. com/JuliaLang/julia/issues/7157).

Quando nenhuma redução é aplicada, um objeto do mesmo tipo e forma que o original seria simplesmente preenchido pela iteração sobre o LazyArray (equivalente a collect , mas respeitando o tipo do array original ). A única coisa que é necessária para isso é que o iterador retorne um objeto que pode ser usado para chamar getindex no LazyArray e setindex! no resultado (por exemplo, linear ou cartesiano coordenadas).

Quando uma redução é aplicada, ele usaria o método de iteração relevante em seu argumento para iterar sobre as dimensões necessárias do LazyArray e preencher uma matriz com o resultado (equivalente a reduce mas usando um iterador personalizado para se adaptar ao tipo de matriz). Uma função (a usada no último parágrafo) retornaria um iterador percorrendo todos os elementos da maneira mais eficiente; outros permitiriam fazê-lo em dimensões específicas.

Todo esse sistema também suportaria operações in loco de forma bastante direta.

Eu estava pensando um pouco em abordar a sintaxe e pensei em .= para aplicar operações elementares ao array.
Então o exemplo de @nalimilan sum(abs.(A).^2)) infelizmente teria que ser escrito em duas etapas:

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

Isso teria a vantagem de ser fácil de ler e significaria que as funções elementares só precisam ser escritas para uma entrada única (ou múltipla) e otimizadas para esse caso, em vez de escrever métodos específicos de matriz.

Claro, nada além de desempenho e familiaridade impede qualquer um de simplesmente escrever map((x) -> abs(x)^2, A) agora como foi dito.

Alternativamente, cercar uma expressão a ser mapeada com .() pode funcionar.
Eu não sei o quão difícil seria fazer isso, mas .sin(x) e .(x + sin(x)) então mapeiam a expressão dentro dos parênteses ou a função que segue o . .
Isso permitiria reduções como o exemplo de @nalimilan onde sum(.(abs(A)^2)) poderia ser escrito em uma única linha.

Ambas as propostas usam um prefixo . que, ao usar broadcast internamente, me fez pensar em operações elementares em arrays. Isso pode ser facilmente trocado por $ ou outro símbolo.
Esta é apenas uma alternativa para colocar um operador de mapa em torno de cada função a ser mapeada e, em vez disso, agrupar a expressão inteira e especificar a que será mapeada.

Eu experimentei a ideia LazyArray que expus no meu último comentário: https://gist.github.com/nalimilan/e737bc8b3b10288abdad

Esta prova de conceito não tem nenhum açúcar sintático, mas (a ./ 2).^2 seria traduzido para o que está escrito na essência como LazyArray(LazyArray(a, /, (2,)), ^, (2,)) . O sistema funciona muito bem, mas precisa de mais otimização para ser competitivo remotamente com loops no que diz respeito ao desempenho. O problema (esperado) parece ser que a chamada de função na linha 12 não é otimizada (quase todas as alocações acontecem lá), mesmo em uma versão onde argumentos adicionais não são permitidos. Acho que preciso parametrizar o LazyArray na função que ele chama, mas não descobri como poderia fazer isso, muito menos lidar com argumentos também. Alguma ideia?

Alguma sugestão sobre como melhorar o desempenho de LazyArray ?

@nalimilan Eu experimentei uma abordagem semelhante há um ano, usando tipos de functor em NumericFuns para parametrizar os tipos de expressão preguiçosos. Tentei uma variedade de truques, mas não tive sorte de preencher a lacuna de desempenho.

A otimização do compilador foi aprimorada gradualmente ao longo do ano passado. Mas ainda sinto que ainda não é capaz de otimizar a sobrecarga desnecessária. Esse tipo de coisa exige que os compiladores tenham funções inline agressivas. Você pode tentar usar @inline e ver se melhora as coisas.

@lindahua @inline não faz nenhuma diferença nos tempos, o que é lógico para mim, pois getindex(::LazyArray, ...) é especializado para uma determinada assinatura LazyArray , que não especifica qual função deve ser chamado. Eu precisaria de algo como LazyArray{T1, N, T2, F} , com F a função que deve ser chamada, para que ao compilar getindex a chamada seja conhecida. Existe uma maneira de fazer isso?

Inlining seria outra grande melhoria, mas no momento os tempos são muito piores do que até mesmo uma chamada não inline.

Você pode considerar usar NumericFuns e F pode ser um tipo de functor.

Dahua

Eu precisei de funções onde eu sei o tipo de retorno para distribuição
computação, onde crio referências ao resultado antes do resultado (e
portanto, seu tipo) é conhecido. Eu mesmo implementei uma coisa muito semelhante, e
provavelmente deve mudar para usar o que você chama de "Functors". (não gosto do
nome "functor", pois geralmente são outra coisa <
http://en.wikipedia.org/wiki/Functor>, mas acho que C++ confundiu as águas
aqui.)

Eu acho que faria sentido dividir sua parte Functor do
funções matemáticas.

-erik

Na quinta-feira, 20 de novembro de 2014 às 10h35, Dahua Lin [email protected]
escrevi:

Você pode considerar usar NumericFuns e F pode ser um tipo de functor.

Responda a este e-mail diretamente ou visualize-o no GitHub
https://github.com/JuliaLang/julia/issues/8450#issuecomment -63826019.

Erik Schnetter [email protected]
http://www.perimeterinstitute.ca/personal/eschnetter/

@lindahua Eu tentei usar functors e, de fato, o desempenho é muito mais razoável:
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) 

Não tenho certeza do que mais pode ser feito para melhorar as coisas, pois o código gerado ainda não parece ideal. Preciso de olhos mais experientes para dizer o que está errado.

Na verdade, o teste acima usou Pow , o que aparentemente dá uma grande diferença de velocidade dependendo se você escreve um loop explícito ou usa um LazyArray . Acho que isso tem a ver com a fusão de instruções que só seriam executadas no último caso. O mesmo fenômeno é visível com, por exemplo, adição. Mas com outras funções a diferença é bem menor, seja com uma matriz 100x100 ou 1000x1000, provavelmente por serem externas e assim o inlining não ganha muito:

# 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)

Então, gostaria de descobrir por que as otimizações não acontecem com LazyArray . A montagem gerada é bastante longa para operações simples. Por exemplo, para 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

Ao contrário do equivalente:

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

A parte até jae L64 é uma verificação de limites de array. Usar @inbounds pode ajudar (se apropriado).

A parte abaixo, onde duas linhas consecutivas começam com mov RAX, ... , é uma dupla indireção, ou seja, acessar um ponteiro para um ponteiro (ou um array de arrays, ou um ponteiro para um array, etc.). Isso pode ter a ver com a representação interna do LazyArray - talvez usar imutáveis ​​(ou representar imutáveis ​​de maneira diferente por Julia) possa ajudar aqui.

De qualquer forma, o código ainda é bastante rápido. Para torná-lo mais rápido, ele precisaria ser embutido no chamador, expondo mais oportunidades de otimização. O que acontece se você chamar essa expressão, por exemplo, de um loop?

Além disso: O que acontece se você desmontar isso não do REPL, mas de dentro de uma função?

Também não posso deixar de notar que a primeira versão realiza um
divisão enquanto a segunda transformou x/2 em uma multiplicação.

Obrigado pelos comentários.

@eschnett LazyArray já é imutável e estou usando @inbounds em loops. Depois de executar o gist em https://gist.github.com/nalimilan/d345e1c080984ed4c89a , você pode verificar o que isso dá em um loop com isso:

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); 

Então, talvez tudo que eu preciso é ser capaz de forçar o inlining? Nas minhas tentativas, adicionar @inline a getindex não altera os tempos.

@toivoh O que poderia explicar que neste último caso a divisão não seja simplificada?

Continuei experimentando com a versão de dois argumentos (chamada LazyArray2 ). Acontece que para uma operação simples como x .+ y , é realmente mais rápido usar um LazyArray2 do que o atual .+ , e também é muito próximo de loops explícitos (estes são para 1000 chamadas , consulte 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)

Portanto, parece que essa estratégia é viável para substituir todas as operações elementares, incluindo os operadores .+ , .* , etc.

Também parece muito competitivo realizar operações comuns, como calcular a soma das diferenças quadradas ao longo de uma dimensão de uma matriz, ou seja, sum((x .- y).^2, 1) (veja novamente a essência):

# 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)

@nalimilan
Sua abordagem com LazyArrays parece ser semelhante à maneira como a fusão a vapor funciona Haskell [1, 2]. Talvez possamos aplicar ideias dessa Área?

[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 Obrigado. Isso é de fato semelhante, mas mais complexo porque Julia usa um modelo imperativo. Pelo contrário, em Haskell, o compilador precisa lidar com uma grande variedade de casos e até mesmo lidar com problemas de SIMD (que são tratados pelo LLVM posteriormente em Julia). Mas honestamente não sou capaz de analisar tudo nestes jornais.

@nalimilan eu conheço a sensação. Achei o segundo artigo particularmente interessante, pois discute Generalized Stream Fusion, que aparentemente permite um bom modelo de computação sobre vetores.

Eu acho que o ponto principal que devemos tirar disso que construções como map e reduce em combinação com preguiça podem ser suficientemente rápidos (ou até mais rápidos que loops explícitos).

Tanto quanto posso dizer, as chaves ainda estão disponíveis na sintaxe de chamada. E se isso se tornasse func{x} ? Talvez um pouco desperdiçado?

No tópico de vetorização rápida (no sentido de SIMD), existe alguma maneira de emular a maneira como Eigen faz isso?

Aqui está uma proposta para substituir todas as operações elementares atuais por uma generalização do que chamei de LazyArray e LazyArray2 acima. Isso, claro, depende da suposição de que podemos fazer isso rápido para todas as funções sem depender de functors de NumericFuns.jl.

1) Adicione uma nova sintaxe f.(x) ou f$(x) ou qualquer outra que crie uma LazyArray chamando f() em cada elemento de x .

2) Generalize essa sintaxe seguindo como broadcast funciona atualmente, de modo que, por exemplo f.(x, y, ...) ou f$(x, y, ...) crie um LazyArray , mas expandindo dimensões singleton de x , y , ... de modo a dar-lhes um tamanho comum. Isso, é claro, seria feito em tempo real por cálculos nos índices, de modo que os arrays expandidos não fossem realmente alocados.

3) Faça .+ , .- , .* , ./ , .^ , etc. use LazyArray em vez de broadcast .

4) Introduza um novo operador de atribuição .= ou $= que transformaria (chamando collect ) um LazyArray em um array real (de um tipo dependendo de sua entradas via regras de promoção e de um tipo de elemento dependendo do tipo de elemento das entradas e da função chamada).

5) Talvez até substitua broadcast por uma chamada para LazyArray e um íon imediato collect dos resultados em um array real.

O ponto 4 é fundamental: as operações elementares nunca retornariam arrays reais, sempre LazyArray s, de modo que ao combinar várias operações, nenhuma cópia é feita e os loops podem ser fundidos para eficiência. Isso permite reduções de chamadas como sum no resultado sem alocar os temporários. Portanto, expressões desse tipo seriam idiomáticas e eficientes, tanto para matrizes densas quanto para matrizes esparsas:

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

Acho que retornar esse tipo de objeto leve se encaixa perfeitamente na nova imagem de visualizações de matriz e Transpose / CTranspose . Isso significa que em Julia você é capaz de realizar operações complexas de forma muito eficiente com uma sintaxe densa e legível, embora em alguns casos você tenha que chamar explicitamente copy quando precisar que um "pseudo-array" seja independente de a matriz real em que se baseia.

Isso realmente soa como um recurso importante. O comportamento atual dos operadores elementares é uma armadilha para novos usuários, pois a sintaxe é boa e curta, mas o desempenho geralmente é terrivelmente ruim, aparentemente pior do que no Matlab. Na semana passada, vários tópicos sobre usuários de julia tiveram problemas de desempenho que desapareceriam com esse design:
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

Para os propósitos deste problema, eu separaria a sintaxe da preguiça. Mas sua proposta é interessante.

Parece chegar um ponto em que há apenas _tantos pontos_. O exemplo do meio em particular seria melhor escrito como

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

que requer apenas funções básicas e um eficiente map ( .|> ).

Esta é uma comparação interessante:

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)]

Se você descontar a parte for ... , a compreensão é apenas um caractere a mais. Eu quase prefiro ter uma sintaxe de compreensão abreviada do que todos esses pontos.

Uma compreensão 1d não preserva a forma, mas agora que temos for i in eachindex(x) isso também pode mudar.

Um problema com as compreensões é que elas não suportam DataArrays.

Acho que pode valer a pena dar uma olhada em um monte de coisas que aconteceram no .Net que se parecem muito com a ideia do LazyArray. Essencialmente, parece muito próximo de uma abordagem de estilo LINQ para mim, onde você tem uma sintaxe que se parece com as coisas elementares que temos agora, mas na verdade essa sintaxe cria uma árvore de expressão e essa árvore de expressão é avaliada posteriormente de alguma maneira eficiente . Isso é de alguma forma perto?

No .Net eles foram longe com essa ideia: você pode executar essas árvores de expressão em paralelo em várias CPUs (adicionando .AsParallel()), ou você pode executá-las em um grande cluster com o DryadLINQ, ou mesmo em um http:// /research.microsoft.com/en-us/projects/accelerator/ (o último pode não ter sido totalmente integrado ao LINQ, mas está próximo em espírito se bem me lembro), ou é claro que poderia ser traduzido para SQL se o os dados estavam nessa forma e você usava apenas operadores que podiam ser convertidos em instruções SQL.

Minha sensação é que o Blaze também está indo nessa direção, ou seja, uma maneira de construir facilmente objetos que descrevem computações, e então você pode ter diferentes mecanismos de execução para isso.

Não tenho certeza de que isso esteja muito claro, mas parece-me que todo esse problema deve ser analisado no contexto de como se pode gerar um código SIMD eficiente de baixo nível e como isso pode ser usado para computação de GPU, clustering, paralelo computação etc

Sim, você está certo que o exemplo mais longo tem muitos pontos. Mas os dois mais curtos são mais típicos e, nesse caso, é importante ter uma sintaxe curta. Eu gostaria de separar sintaxe de preguiça, mas como seus comentários mostram parece ser muito difícil, sempre misturamos os dois!

Pode-se imaginar adaptar a sintaxe de compreensão, algo como y = [sqrt(x + 2) over x] . Mas, como @johnmyleswhite observou, eles devem suportar DataArrays , mas também matrizes esparsas e qualquer novo tipo de matriz. Portanto, este é novamente um caso de mistura de sintaxe e recursos.

Mais fundamentalmente, acho que duas características que minha proposta oferece sobre as alternativas são:
1) Suporte para atribuição no local sem alocação usando y[:] = sqrt.(x .+ 2) .
2) Suporte para reduções sem alocação como sum((x .- y).^2, 1) .

Isso poderia ser fornecido com outras soluções (desconsiderando problemas de sintaxe)?

@davidanthoff Obrigado, olhando para isso agora (acho que LazyArray poderia ser feito para suportar computação paralela também).

Talvez isso possa ser combinado com Generators - eles também são uma espécie de array preguiçoso. Eu gosto um pouco da sintaxe de compreensão [f(x) over x] , embora possa ser conceitualmente difícil para os recém-chegados (já que o mesmo nome está sendo efetivamente usado para os elementos e o próprio array). Se as compreensões sem colchetes criassem um gerador (como eu brinquei há muito tempo ), seria natural usar essas novas compreensões sobre o estilo x sem colchetes para retornar um LazyArray em vez de coletá-lo imediatamente.

@mbauman Sim, geradores e arrays preguiçosos compartilham muitas propriedades. A ideia de usar colchetes para coletar um array gerador/preguiçoso e não adicioná-los para manter o objeto preguiçoso parece legal. Então, em relação aos meus exemplos acima, seria possível escrever tanto 1) y[:] = sqrt(x + 2) over x quanto sum((x - y)^2 over (x, y), 1) (embora eu ache natural mesmo para iniciantes, vamos deixar a questão de over para a sessão de bikeshedding e concentre-se primeiro nos fundamentos).

Eu gosto da idéia f(x) over x . Poderíamos até usar f(x) for x para evitar uma nova palavra-chave. Na verdade [f(x) for x=x] já funciona. Precisaríamos então fazer as compreensões equivalentes a map , para que elas possam funcionar para não Arrays. Array seria apenas o padrão.

Devo admitir que também passei a gostar da ideia over . Uma diferença entre over como mapa e for na compreensão da lista é o que acontece no caso de vários iteradores: [f(x, y) for x=x, y=y] resulta em uma matriz. Para o caso do mapa, você geralmente ainda quer um vetor, ou seja, [f(x, y) over x, y] seria equivalente a [f(x, y) for (x,y) = zip(x, y)]] . Por causa disso, ainda acho que vale a pena introduzir uma palavra-chave adicional over , porque, como esse problema levantou, map sobre vários vetores é muito comum e precisa ser conciso.

Ei, eu convenci Jeff sobre a sintaxe! ;-)

Isso pertence ao lado de #4470, então adicionando aos projetos 0.4 por enquanto.

Se eu entendo a essência da discussão, o principal problema é que queremos obter uma sintaxe semelhante a mapeamento que:

  • trabalha com vários tipos de dados, como DataArrays, não apenas arrays nativos;
  • é tão rápido quanto o loop escrito manualmente.

Pode ser possível fazer isso usando inlining, mas tendo muito cuidado para garantir que o inlining funcione.

Que tal uma abordagem diferente: usando macro dependendo do tipo de dados inferido. Se pudermos inferir que a estrutura de dados é DataArray, usamos map-macro fornecido pela biblioteca DataArrays. Se for SomeKindOfStream, usamos a biblioteca de fluxo fornecida. Se não pudermos inferir o tipo, usamos apenas a implementação geral despachada dinamicamente.

Isso pode forçar os criadores de estruturas de dados a escrever tais macros, mas seria necessário apenas se seu autor quisesse que tivesse uma execução realmente eficiente.

Se o que estou escrevendo não estiver claro, quero dizer que algo como [EXPR for i in collection if COND] pode ser traduzido para eval(collection_mapfilter_macro(:(i), :(EXPR), :(COND))) , onde collection_mapfilter_macro é escolhido com base no tipo de coleção inferido.

Não, não queremos fazer coisas assim. Se DataArray define map (ou o equivalente), sua definição deve sempre ser chamada para DataArrays, independentemente do que pode ser inferido.

Este problema na verdade não é sobre implementação, mas sintaxe. No momento, muitas pessoas estão acostumadas a sin(x) mapear implicitamente se x for um array, mas há muitos problemas com essa abordagem. A questão é qual sintaxe alternativa seria aceitável.

1) Suporte para atribuição no local sem alocação usando y[:] = sqrt.(x .+ 2)
2) Suporte para reduções sem alocação como sum((x .- y).^2, 1)

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

Olhando para esses três exemplos de outros, acho que com a sintaxe for isso ficaria assim:
1) y[:] = [ sqrt(x + 2) for x ])
2) sum([ (x-y)^2 for x,y ], 1)
e
y = [ √π exp(-x^2) * sin(k*x) + im * log(x-1) for x,k ]

Eu gosto bastante disso! O fato de criar um array temporário é bastante explícito e ainda é legível e conciso.

Pergunta menor, porém, poderia x[:] = [ ... for x ] ter alguma mágica para alterar o array sem alocar um temporário?
Não tenho certeza se isso traria muitos benefícios, mas posso imaginar que ajudaria em grandes matrizes.
Posso acreditar, porém, que pode ser uma chaleira de peixe completamente diferente que deveria ser discutida em outro lugar.

@Mike43110 Seu x[:] = [ ... for x ] poderia ser escrito x[:] = (... for x) , o RHS criando um gerador, que seria coletado elemento por elemento para preencher x , sem alocar uma cópia. Essa foi a ideia por trás do meu experimento LazyArray acima.

A sintaxe [f <- y] seria boa se combinada com uma sintaxe Int[f <- y] para um mapa que conhece seu tipo de saída e não precisa interpolar de f(y[1]) quais serão os outros elementos.

Especialmente, como isso também fornece uma interface intuitiva para mapslices , [f <- rows(A)] onde rows(A) (ou columns(A) ou slices(A, dims) ) retorna um Slice objeto para que o dispatch possa ser usado:

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

Quando você adiciona indexação, isso fica um pouco mais difícil. Por exemplo

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

É difícil igualar a concisão disso. A explosão do estilo de compreensão é muito ruim:

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

onde, para piorar as coisas, era necessária inteligência para saber que este é um caso de iteração aninhada e não pode ser feito com um único over . Embora se f e g forem um pouco caros, isso pode ser mais rápido:

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

mas ainda mais.

Esse tipo de exemplo parece defender "pontos", pois isso poderia dar f.(x[:,j]) .* g.(x[i,:]) .

@JeffBezanson Não tenho certeza de qual é a intenção do seu comentário. Alguém sugeriu se livrar da sintaxe .* ?

Não; Estou focando em f e g aqui. Este é um exemplo onde você não pode simplesmente adicionar over x no final da linha.

Ok, eu vejo, eu tinha perdido o final do comentário. Na verdade, a versão de pontos é melhor nesse caso.

Embora com exibições de matriz, haverá uma alternativa razoavelmente eficiente (AFAICT) e não tão feia:
[ f(y) * g(z) for y in x[:,j], z in x[i,:] ]

O exemplo acima poderia ser resolvido aninhando sobre palavras-chave?

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

é interpretado como

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

enquanto

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

torna-se

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

Então, o exemplo específico acima seria algo como

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

EDIT: Em retrospecto, isso não é tão conciso quanto eu pensei que seria.

@JeffBezanson Que tal

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

dá o equivalente a f.(x[:,n] .* g.(x[m,:]) . A nova sintaxe x[:i,n] significa que i está sendo introduzido localmente como um iterador sobre os índices do contêiner x[:,n] . Não sei se é possível implementar. Mas não parece (prima facie) nem feio nem complicado, e a própria sintaxe dá limites para o iterador, ou seja, 1:length(x[:,n]). No que diz respeito às palavras-chave, "over" pode sinalizar que todo o intervalo deve ser usado, enquanto "for" pode ser usado se o usuário desejar especificar um subintervalo de 1:length(x[:,n]):

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

@davidagold , :i já significa o símbolo i .

Ah sim, bom ponto. Bem, desde que os pontos sejam um jogo justo, que tal

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

onde o ponto indica que i está sendo introduzido localmente como um iterador em 1:length(x[:,n). Suponho que, em essência, isso mude a notação de ponto de modificar as funções para modificar as matrizes, ou melhor, seus índices. Isso salvaria um do "dot creep" Jeff observou:

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

em oposição a

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

embora eu suponha que o último seja um pouco mais curto. [EDIT: também, se for possível omitir o over i quando não houver ambiguidade, então, na verdade, tem-se uma sintaxe um pouco mais curta:

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

Uma vantagem potencial da sintaxe de compreensão é que ela pode permitir uma gama mais ampla de padrões de operação elementar. Por exemplo, se o analisador entendeu que a indexação com i em x[m, .i] é implicitamente comprimento do módulo (x[m,:]), então pode-se escrever

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

Para multiplicar os elementos de x contra os elementos de y na ordem oposta, ou seja, o primeiro elemento de x contra o último elemento de y , etc . Pode-se escrever

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

para multiplicar o i elemento de x pelo i+1 elemento de y (onde o último elemento de x será multiplicado pelo primeiro elemento de y devido à indexação ser entendida neste contexto como comprimento do módulo(x)). E se p::Permutation permuta (1, ..., comprimento(x)) pode-se escrever

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

para multiplicar o i º elemento de x pelo p(i) º elemento de y .

De qualquer forma, isso é apenas uma humilde opinião de um estranho sobre uma questão totalmente especulativa. =p Eu aprecio o tempo que alguém leva para considerar isso.

Uma versão aprimorada de vetorização que usará a reciclagem de estilo r pode ser bastante útil. Ou seja, argumentos que não correspondem ao tamanho do maior argumento são estendidos via reciclagem. Então os usuários podem facilmente vetorizar o que quiserem, independentemente do número de argumentos etc.

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]

Eu tendo a pensar que a reciclagem troca muita segurança por conveniência. Com a reciclagem, é muito fácil escrever código com bugs que seja executado sem gerar erros.

A primeira vez que li sobre esse comportamento de R, imediatamente me perguntei por que alguém pensaria que isso era uma boa ideia. Isso é uma coisa realmente estranha e surpreendente de se fazer implicitamente em arrays de tamanhos incompatíveis. Pode haver um punhado de casos em que é assim que você deseja estender o array menor, mas também pode querer preenchimento de zero ou repetir os elementos finais, ou extrapolação, ou um erro, ou qualquer número de outros aplicativos dependentes escolhas.

Se deve ou não usar @super_vectorize seria colocado nas mãos do usuário. Também seria possível dar avisos para vários casos. Por exemplo, em R,

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

Não tenho objeções em tornar isso uma coisa opcional que os usuários podem escolher se querem ou não usar, mas não precisa ser implementado na linguagem base quando pode ser feito em um pacote.

@vectorize_1arg e @vectorize_2arg já estão incluídos no Base, e as opções que eles dão ao usuário parecem um pouco limitadas.

Mas esta questão está focada no projeto de um sistema para remover @vectorize_1arg e @vectorize_2arg da Base. Nosso objetivo é remover funções vetorizadas da linguagem e substituí-las por uma melhor abstração.

Por exemplo, a reciclagem pode ser escrita como

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

o que para mim é bem próximo da maneira ideal de escrevê-lo. Ninguém precisa construir isso para você. As principais questões são (1) isso pode ser mais conciso, (2) como estendê-lo para outros tipos de contêiner.

Olhando a proposta do @davidagold fiquei imaginando se var: não poderia ser usado para aquele tipo de coisa onde a variável seria o nome antes dos dois pontos. Eu vi essa sintaxe usada para significar A[var:end] , então parece estar disponível.

f(x[:,j]) .* g(x[i,:]) seria então f(x[a:,j]) * g(x[i,b:]) for a, b que não é muito pior.

Vários dois pontos seriam um pouco estranhos.

f(x[:,:,j]) .* g(x[i,:,:]) -> f(x[a:,a:,j]) * g(x[i,b:,b:]) for a, b foi meu pensamento inicial sobre isso.

Ok, então aqui está uma breve tentativa de um programa de reciclagem. Deve ser capaz de lidar com matrizes n-dimensionais. Provavelmente seria possível incorporar tuplas de forma análoga a vetores.

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)

Claramente, este não é o código mais elegante ou rápido (sou um imigrante R recente). Não tenho certeza de como ir daqui para uma macro @vectorize .

EDIT: loop redundante combinado
EDIT 2: separou a coação ao tamanho. atualmente só funciona para 0-2 dimensões.
EDIT 3: Uma maneira um pouco mais elegante de fazer isso seria definir um tipo especial de array com indexação de mod. Isso é,

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

EDIT 4: Coisas que eu estou querendo saber existem porque isso foi difícil de escrever: uma generalização n-dimensional de hcat e vcat? Uma maneira de preencher uma matriz n-dimensional (correspondendo ao tamanho de uma determinada matriz) com listas ou tuplas dos índices de cada posição específica? Uma generalização n-dimensional de repmat?

[pao: realce de sintaxe]

Você realmente não quer definir funções com a sintaxe foo = function(x,y,z) ... end em Julia, embora funcione. Isso cria uma ligação não constante do nome a uma função anônima. Em Julia, a norma é usar funções genéricas e as ligações às funções são automaticamente constantes. Caso contrário, você terá um desempenho terrível.

Não vejo por que repmat é necessário aqui. Arrays preenchidos com o índice de cada posição também são um sinal de alerta: não deveria ser necessário usar uma grande quantidade de memória para representar tão pouca informação. Acredito que tais técnicas sejam realmente úteis apenas em linguagens onde tudo precisa ser "vetorizado". Parece-me que a abordagem correta é apenas executar um loop onde alguns índices são transformados, como em https://github.com/JuliaLang/julia/issues/8450#issuecomment -111898906.

Sim, isso faz sentido. Aqui está um começo, mas estou tendo problemas para descobrir como fazer o loop no final e depois fazer uma macro @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

Na palestra, @JeffBezanson mencionou a sintaxe sin(x) over x , por que não algo mais como:
sin(over x) ? (ou use algum caractere em vez de ter over como palavra-chave)

Uma vez que isso seja resolvido, também podemos resolver #11872

Espero não estar atrasado para a festa, mas gostaria apenas de oferecer um +1 à proposta de sintaxe do @davidagold . É conceitualmente claro, conciso e parece muito natural escrever. Não tenho certeza se . seria o melhor caractere de identificação, ou quão viável seria uma implementação real, mas pode-se fazer uma prova de conceito usando uma macro para experimentá-la (essencialmente como @devec , mas pode até ser mais fácil de implementar).

Ele também tem o benefício de "encaixar" com a sintaxe de compreensão de array existente:

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

vs.

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

A principal diferença entre os dois é que o primeiro teria mais restrições à preservação da forma, pois implica um mapa.

over , range e window têm alguma arte anterior no espaço OLAP como modificadores da iteração, isso parece consistente.

Eu não estou interessado na sintaxe . , pois isso parece um rastejo para o ruído da linha.

$ talvez seja consistente, interno os valores de iteração i,j na expressão?

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

Para vetorização automática de uma expressão, não podemos taint um dos vetores na expressão e fazer com que o sistema de tipos levante a expressão para o espaço vetorial?

Faço semelhante a operações de séries temporais onde a expressividade de julia já me permite escrever

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

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

que quando observado produz uma série temporal de vetores.

A parte principal que falta é a capacidade de elevar automaticamente as funções existentes para o espaço. Isso tem que ser feito a mão

Essencialmente, eu gostaria de poder definir algo como o seguinte ...

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

e então ser capaz de se especializar para operações específicas

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

Olá @JeffBezanson ,

Se bem entendi, gostaria de propor uma solução para seu comentário JuliaCon 2015 sobre um comentário feito acima:
"[...] E dizer aos escritores da biblioteca para colocar @vectorize em todas as funções apropriadas é bobagem; você deve ser capaz de escrever apenas uma função, e se alguém quiser calculá-la para cada elemento, use map."
(Mas não vou abordar a outra questão fundamental "[..] nenhuma razão realmente convincente para que sin, exp etc.

Em Julia v0.40, consegui obter uma solução um pouco melhor (na minha opinião) do que @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!

Isso é mais ou menos razoável, mas é um pouco semelhante à solução @vectorize . Para que a vetorização seja elegante, sugiro que Julia dê suporte ao seguinte:

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

É isso! Ter uma função herdada de uma função Vetorizável a tornaria vetorizável.

Espero que esteja na linha do que você estava procurando.

Cumprimentos,

MA

Na ausência de herança múltipla, como uma função herda de Vectorizable e de outra coisa? E como você relaciona as informações de herança de métodos específicos com as informações de herança de uma função genérica?

@ma-laforge Você já pode fazer isso --- defina um tipo myadd <: Vectorizable2Arg , então implemente call para myadd em Number .

Obrigado por isso @JeffBezanson!

De fato, posso quase minha solução parecer quase tão boa quanto o que eu quero:

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

Agora, a única coisa que faltaria seria uma maneira de "subdigitar" qualquer função, para que toda a seção F pudesse ser substituída pela sintaxe mais elegante:

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

NOTA: Eu fiz o tipo "MyAddType" e o nome da função em um objeto singleton "myadd" porque acho a sintaxe resultante mais agradável do que se alguém estivesse usando Type{Vectorizable2Arg} na assinatura de chamada:

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

Infelizmente , pela sua resposta, parece que isso _não_ seria uma solução adequada para a "bobagem" da macro @vectorize .

Cumprimentos,

MA

@johnmyleswhite :

Gostaria de responder ao seu comentário, mas acho que não entendi. Você pode esclarecer?

Uma coisa eu _posso_ dizer:
Não há nada de especial em "Vetorizável". A ideia é que qualquer pessoa possa definir sua própria "classe" de função (Ex: MyFunctionGroupA<:Function ). Eles poderiam então capturar chamadas para funções desse tipo definindo sua própria assinatura de "chamada" (como demonstrado acima).

Dito isto: Minha sugestão é que as funções definidas dentro do Base usem Base.Vectorizable <: Function (ou algo similar) para gerar automaticamente algoritmos vetorizados automaticamente.

Eu sugeriria então que os desenvolvedores de módulos implementassem suas próprias funções usando um padrão como:

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

Claro, eles teriam que fornecer sua própria versão de promote_type(::Type{MyType},::Type{MyType}) - se o padrão já não for retornar MyType .

Se o algoritmo de vetorização padrão for insuficiente, nada impede o usuário de implementar sua própria hierarquia:

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

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

MA

@ma-laforge, Desculpe por não ser claro. Minha preocupação é que qualquer hierarquia sempre carecerá de informações importantes porque Julia tem herança única, o que exige que você se comprometa com um único tipo pai para cada função. Se você usar algo como myfunction(x::MyType, y::MyType) <: Base.Vectorizable , sua função não se beneficiará de outra pessoa definindo um conceito como Base.NullableLiftable que gera automaticamente funções de valores nulos.

Parece que isso não seria um problema com características (cf. https://github.com/JuliaLang/julia/pull/13222). Também relacionada está a nova possibilidade de declarar métodos puros (https://github.com/JuliaLang/julia/pull/13555), o que poderia implicar automaticamente que tal método é vetorizável (pelo menos para métodos de argumento único).

@johnmyleswhite ,

Se bem entendi: não acho que isso seja um problema para _este_ caso em particular. Isso porque estou propondo um padrão de design. Suas funções não _tem_ para herdar de Base.Vectorizable ... Você pode usar as suas próprias.

Eu realmente não sei muito sobre NullableLiftables (eu não pareço ter isso na minha versão de Julia). No entanto, supondo que herde de Base.Function (o que também não é possível na minha versão de Julia):

NullableLiftable <: Function

Seu módulo poderia então implementar (apenas uma vez) um subtipo _new_ vetorizável:

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

Então, a partir de agora, qualquer pessoa que defina uma função <: VectorizableNullableLiftable terá seu código de vetorização aplicado automaticamente!

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

Eu entendo, que ter mais de um tipo Vectorizável ainda é um pouco chato (e um pouco deselegante)... função com uma chamada para @vectorize_Xarg).

(1) Isso supondo que Julia suporta herança em funções (ex: myfunction(...)<: Vectorizable ) - o que não parece, na v0.4.0. A solução que consegui trabalhando em Julia 0.4.0 é apenas um hack... Você ainda tem que registrar sua função... não muito melhor do que chamar @vectorize_Xarg

MA

Eu ainda acho que é meio que a abstração errada. Uma função que pode ou deve ser "vetorizada" não é um tipo específico de função. A função _Every_ pode ser passada para map , dando a ela esse comportamento.

BTW, com a mudança que estou trabalhando no ramo jb/functions, você poderá fazer function f(x) <: T (embora, claramente, apenas para a primeira definição de f ).

Ok, acho que entendi melhor o que você procura... e não é o que sugeri. Acho que isso pode ser parte dos problemas que @johnmyleswhite teve com minhas sugestões também...

...Mas se agora eu entendo qual é o problema, a solução parece ainda mais simples para mim:

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

Como myadd é do tipo Function , ele deve ficar preso pela função call ... o que ele faz:

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

Mas call não despacha automaticamente em funções, por algum motivo (não tenho certeza do porquê):

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

Mas imagino que esse comportamento não deve ser muito difícil de mudar . Pessoalmente, não sei como me sinto em fazer essas funções abrangentes, mas parece que é isso que você quer.

Algo estranho que notei: Julia já vetoriza automaticamente funções se não forem digitadas:

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

RE: BTW...:
Frio! Eu me pergunto que coisas legais eu serei capaz de fazer subdigitando funções :).

Julia já vetoriza automaticamente funções se não forem digitadas

Parece assim porque o operador + usado dentro da função é vetorizado.

Mas imagino que esse comportamento não deve ser muito difícil de mudar. Pessoalmente, não sei como me sinto em fazer essas funções abrangentes, mas parece que é isso que você quer.

Partilho as suas reservas. Você não pode sensatamente ter uma definição que diga "aqui está como chamar qualquer função", porque cada função diz o que fazer quando é chamada --- isso é o que é uma função!

Eu deveria ter dito: ... operadores infixos não-unicode definidos pelo usuário, pois não acho que queremos exigir que os usuários digitem caracteres unicode para acessar essa funcionalidade principal. Embora eu veja que $ é realmente um dos adicionados, então obrigado por isso! Uau, então isso realmente funciona em Julia hoje (mesmo que não seja "rápido" ... ainda):

julia> ($) = mapa
julia> sin $ (0,5 * (abs2 $ (xy)))

@binarybana que tal / \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

Há também:

FWIW, eu pelo menos inicialmente assumiria que \mapsto era uma sintaxe alternativa para lambdas, como é comumente usado em matemática e, essencialmente (em sua encarnação ASCII, -> ) em Julia também . Acho que isso ficaria meio confuso.

Falando em matemática… Na teoria dos modelos eu vi map expresso pela aplicação de uma função a uma tupla sem parênteses. Ou seja, se \bar{a}=(a_1, \dots, a_n) , então f(\bar{a}) é f(a_1, \dots, a_n) (ou seja, essencialmente, apply ) e f\bar{a} é (f(a_1), \dots, f(a_n)) (ou seja, map ). Sintaxe útil para definir homomorfismos, etc., mas não tão facilmente transferível para uma linguagem de programação :-}

Que tal qualquer uma das outras alternativas como \Mapsto , você confundiria com => (Par)? Eu acho que ambos os símbolos são distinguíveis aqui lado a lado:

  • ->

Existem muitos símbolos que se parecem, qual seria a razão de ter tantos deles se usássemos apenas aqueles que parecem muito diferentes ou são puros ASCII?

Acho que isso ficaria meio confuso.

Acho que documentação e experiência resolvem isso, concorda?

Existem também muitas outras setas como símbolos, eu honestamente não sei para que eles são usados ​​em matemática ou então, eu só propus esses porque eles têm map em seus nomes! :sorrir:

Acho que meu ponto é que -> é a tentativa de Julia de representar em ASCII. Portanto, usar para significar outra coisa parece desaconselhável. Não é que eu não possa distingui-los visualmente :-)

Meu pressentimento é apenas que, se vamos usar símbolos matemáticos bem estabelecidos, podemos querer pelo menos pensar em como o uso de Julia difere do uso estabelecido. Parece lógico selecionar símbolos com map em seus nomes, mas neste caso isso se refere à definição de um mapa (ou de mapas de diferentes tipos, ou os tipos de tais mapas). Isso também é verdade para o uso em Pair, mais ou menos, onde em vez de definir uma função completa definindo para o que um parâmetro mapeia, você realmente lista para o que um determinado argumento (valor de parâmetro) mapeia - ou seja, é um elemento de um explicitamente função enumerada, conceitualmente (por exemplo, um dicionário).

@Ismael-VC O problema com sua sugestão é que você precisa chamar map duas vezes, enquanto a solução ideal não envolveria arrays temporários e reduziria para map((a, b) -> sin(0.5 * abs2(a-b)), x, y) . Além disso, repetir duas vezes não é ótimo tanto visualmente quanto para digitar (para o qual seria bom ter um equivalente em ASCII).

Os usuários de R podem não gostar dessa ideia, mas se avançarmos no sentido de depreciar a análise de caso especial infix-macro atual de ~ (pacotes como GLM e DataFrames precisariam mudar para análise macro de suas fórmulas DSL, ref https:/ /github.com/JuliaStats/GLM.jl/issues/116), que liberaria a mercadoria rara de um operador ascii infixo.

a ~ b poderia ser definido na base como map(a, b) , e talvez a .~ b pudesse ser definido como broadcast(a, b) ? Se for analisado como um operador infixo convencional, as DSLs de macro como uma emulação da interface de fórmula R seriam livres para implementar sua própria interpretação do operador dentro das macros, como o JuMP faz com <= e == .

Talvez não seja a sugestão mais bonita, mas também não são as taquigrafias do Mathematica se você as usar demais... minha favorita é .#&/@

:+1: para um invólucro menos especial e mais generalidade e consistência, os significados que você propõe para ~ e .~ parecem ótimos para mim.

+1 Tony.

@tkelman Para ser claro, como você escreveria, por exemplo sin(0.5 * abs2(a-b)) de uma maneira totalmente vetorizada?

Com uma compreensão, provavelmente. Eu não acho que o mapa infix funcionaria para varargs ou in-place, então a possibilidade de sintaxe livre não resolve todos os problemas.

Então isso realmente não resolveria esse problema. :-/

Até agora, a sintaxe sin(0.5 * abs2(a-b)) over (a, b) (ou uma variante, possivelmente usando um operador infixo) é a mais atraente.

O título desta edição é "Sintaxe alternativa para map(func, x) ". Usar um operador infixo não resolve a fusão de mapa/loop para eliminar temporários, mas acho que pode ser um problema ainda mais amplo, relacionado, mas tecnicamente separado, do que a sintaxe.

Sim, concordo com @tkelman , o ponto é ter uma sintaxe alternativa para map , por isso sugeri usar \mapsto , . O que @nalimilan menciona parece ser mais amplo e mais adequado para outra questão IMHO, How to fully vecotrize expressions ?

<rambling>
Este problema está acontecendo há mais de um ano (e pode continuar indefinidamente como muitos outros problemas estão acontecendo agora)! Mas poderíamos ter Alternative syntax for map(func, x) agora . Dos ±450 contribuidores julianos, apenas 41 conseguiram encontrar este problema e/ou dispostos a compartilhar uma opinião (isso é muito para um problema do github, mas claramente não é suficiente neste caso), apesar de tudo, não há sugestões muito diferentes (que não são apenas pequenas variações do mesmo conceito).

Eu sei que alguns de vocês não gostam da ideia ou não veem valor em fazer pesquisas/enquetes (:chocado:), mas como não preciso pedir permissão a ninguém para algo assim, farei isso de qualquer maneira. É meio triste a forma como não estamos aproveitando ao máximo nossa comunidade e redes sociais e outras comunidades também mais triste ainda que não estamos vendo o valor disso, vamos ver se consigo reunir opiniões mais diferentes e frescas , ou pelo menos verificar para fora o que a maioria sente sobre as opiniões atuais sobre esta questão em particular, como um experimento e ver como vai. Talvez seja realmente inútil, talvez não, só há uma maneira de realmente saber.
</rambling>

@Ismael-VC: Se você realmente deseja fazer uma enquete, a primeira coisa que deve fazer é considerar cuidadosamente a pergunta que deseja fazer. Você não pode esperar que todos leiam o tópico inteiro e resumam as opções que foram discutidas individualmente.

map(func, x) também cobre coisas como map(v -> sin(0.5 * abs2(v)), x) , e é isso que este tópico discutiu. Não vamos mover isso para outro tópico, pois isso tornaria mais difícil manter em mente todas as propostas discutidas acima.

Não me oponho a adicionar sintaxe para o caso simples de aplicar uma função genérica usando map , mas se fizermos isso, acho que seria uma boa ideia considerar a imagem mais ampla ao mesmo tempo. Se não fosse por isso, o problema já poderia ter sido corrigido há muito tempo.

@Ismael-VC Enquetes provavelmente não ajudarão aqui IMHO. Não estamos tentando descobrir qual das várias soluções é a melhor, mas sim encontrar uma solução que ninguém realmente encontrou. Esta discussão já é longa e envolveu muitas pessoas, não acho que adicionar mais ajudará.

@Ismael-VC Tudo bem, sinta-se à vontade para fazer uma enquete. Na verdade, fiz algumas pesquisas de doodle sobre questões no passado (por exemplo, http://doodle.com/poll/s8734pcue8yxv6t4). Na minha experiência, as mesmas ou menos pessoas votam em enquetes do que discutem em tópicos. Faz sentido para questões muito específicas, muitas vezes superficiais/sintaxe. Mas como uma enquete vai gerar novas ideias quando tudo o que você pode fazer é escolher entre as opções existentes?

Claro que o objetivo real desta edição é eliminar funções implicitamente vetorizadas. Em teoria, a sintaxe para map é suficiente para isso, porque todas essas funções estão apenas fazendo map em todos os casos.

Eu tentei procurar por notação matemática existente para isso, mas você tende a ver comentários no sentido de que a operação é muito sem importância para ter notação! No caso de funções arbitrárias em um contexto matemático quase posso acreditar nisso. No entanto, o mais próximo parece ser a notação de produto Hadamard, que tem algumas generalizações: https://en.wikipedia.org/wiki/Hadamard_product_ (matrices)#Analogous_Operations

Isso nos deixa com sin∘x como @rfourquet sugeriu. Não parece muito útil, pois requer unicode e não é amplamente conhecido de qualquer maneira.

@nalimilan , eu acho que você faria apenas sin(0.5 * abs2(a-b)) ~ (a,b) que se traduziria em map((a,b)->sin(0.5 * abs2(a-b)), (a,b)) . Não tenho certeza se isso está certo, mas acho que funcionaria.

Também estou desconfiado de me aprofundar muito no problema do deixe-me-dar-te-uma-expressão-enorme-complicada-e-você-perfeitamente-auto-vetorize-para-me. Acho que a solução definitiva nesse sentido é construir um DAG completo de expressões/tarefas + planejamento de consulta, etc. Mas acho que isso é muito mais difícil do que apenas ter uma sintaxe conveniente para map .

@quinnj Sim, essa é essencialmente a sintaxe over proposta acima, exceto com um operador infixo.

Comentário sério: acho que você provavelmente reinventará o SQL se seguir essa ideia o suficiente, já que o SQL é essencialmente uma linguagem para compor funções elementares de muitas variáveis ​​que são posteriormente aplicadas por meio de "vetorização" em linhas.

@johnmyleswhite concorda, começa a parecer um DSL aka Linq

Nos tópicos postados, você pode especializar o operador |> 'pipe' e obter a funcionalidade de estilo de mapa. Você pode lê-lo como canalizar a função para os dados. Como um bônus extra, você pode usar o mesmo para realizar a composição de funções.

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 Isso é verdade, mas existem objetivos intermediários que valem a pena e são bastante modestos. No meu branch, a versão map de expressões vetorizadas multi-operação já é mais rápida do que temos agora. Portanto, descobrir como fazer uma transição suave para ele é um pouco urgente.

@johnmyleswhite Não tenho certeza. Muito do SQL é sobre selecionar, ordenar e mesclar linhas. Aqui estamos falando apenas sobre a aplicação de uma função em elementos. Além disso, o SQL não fornece nenhuma sintaxe para distinguir reduções (por exemplo SUM ) de operações elementares (por exemplo > , LN ). Os últimos são simplesmente vetorizados automaticamente, assim como em Julia atualmente.

@JeffBezanson A beleza do uso de \circ é que, se você interpretar uma família indexada como uma função do conjunto de índices (que é a "implementação" matemática padrão), o mapeamento _é_ simplesmente composição. Então (sin ∘ x)(i)=sin(x(i)) , ou melhor, sin(x[i]) .

O uso do pipe, como @mdcfrancis menciona, seria essencialmente apenas uma composição de "ordem do diagrama", que geralmente é feita com um ponto e vírgula (possivelmente gordo) em matemática (ou especialmente aplicações CS da teoria das categorias) - mas já temos o pipe operador, é claro.

Se nenhum desses operadores de composição estiver correto, então pode-se usar alguns outros. Por exemplo, pelo menos alguns autores usam o humilde \cdot para composição abstrata de seta/morfismo, pois é essencialmente a "multiplicação" do grupoide (mais ou menos) de setas.

E se alguém quisesse um análogo ASCII: também há autores que realmente usam um ponto para indicar a multiplicação. (Eu posso ter visto alguns usá-lo para composição também; não me lembro.)

Então, alguém poderia ter sin . x … mas acho que seria confuso :-}

Ainda assim... essa última analogia pode ser um argumento para uma das propostas realmente iniciais, ou seja, sin.(x) . (Ou talvez isso seja absurdo.)

Vamos tentar de um ângulo diferente, não atire em mim.

Se definirmos .. por collect(..(A,B)) == ((a[1],..., a[n]), (b[1], ...,b[n])) == zip(A,B) , então, usando T[x,y,z] = [T(x), T(y), T(z)] formalmente, fica que

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

Isso motiva pelo menos uma sintaxe para map que não interfere na sintaxe para construção de arrays. Com :: ou table a extensão f[::(A,B)] = [f(a[i], b[j]) for i in 1:n, j in 1:n] leva pelo menos a um segundo caso de uso interessante.

considere cuidadosamente a pergunta que deseja fazer.

@toivoh Obrigado, eu vou. Atualmente, estou avaliando vários softwares de pesquisa/pesquisa. Também só vou pesquisar sobre a sintaxe preferida, aqueles que querem ler o tópico inteiro vão fazer isso, não vamos assumir que ninguém mais estará interessado em fazer isso.

encontrar uma solução que ninguém realmente encontrou

@nalimilan ninguém entre nós, isso é. :sorrir:

como uma enquete vai gerar novas ideias quando tudo o que você pode fazer é escolher entre as opções existentes?
as mesmas ou menos pessoas votam em enquetes do que discutem em tópicos de discussão.

@JeffBezanson Fico feliz em saber que você já fez enquetes, continue!

  • Como você tem promovido suas enquetes?
  • A partir do software de enquete/pesquisa que avaliei até agora, o kwiksurveys.com permite que os usuários adicionem suas próprias opiniões em vez da opção _nenhuma dessas opções é para mim_.

sin∘x Não parece muito útil, pois requer Unicode e não é amplamente conhecido de qualquer maneira.

Temos tantos Unicode, vamos usá-lo, temos até uma maneira legal de usá-los com tabulação, o que há de errado em usá-los então?, Se não é conhecido, vamos documentar e educar, se não existe, qual é inventando isso? Será que realmente precisamos esperar que alguém o invente e o use para que possamos tomá-lo como precedente e só depois considerá-lo?

tem precedente, então o problema é que é Unicode? porque? quando vamos começar a usar o resto do Unicode não muito conhecido então? Nunca?

Por essa lógica, Julia não é muito conhecida, mas quem quiser aprender, vai. Simplesmente não faz sentido para mim, na minha humilde opinião.

Justo, não sou totalmente contra . Exigir que o unicode use um recurso bastante básico é apenas uma marca contra isso. Não necessariamente o suficiente para afundá-lo completamente.

Seria totalmente louco usar/sobrecarregar * como uma alternativa ASCII? Eu diria que pode ser argumentado que isso faz algum sentido matematicamente, mas acho que às vezes pode ser difícil map seu significado map map já é uma alternativa ASCII, não?)

A beleza do uso de \circ é que, se você interpretar uma família indexada como uma função do conjunto de índices (que é a "implementação" matemática padrão), o mapeamento _é_ simplesmente composição.

Não tenho certeza se compro isso.

@hayd Qual parte disso? Que uma família indexada (por exemplo, uma sequência) pode ser vista como uma função do conjunto de índices, ou que o mapeamento sobre ela se torna composição? Ou que esta é uma perspectiva útil neste caso?

Os dois primeiros pontos (matemáticos) são bastante incontroversos, eu acho. Mas, sim, eu não vou defender fortemente o uso disso aqui – foi principalmente um "Ah, isso meio que se encaixa!" reação.

@mlhetland |> está bem próximo de -> e funciona hoje - também tem a 'vantagem' de ser associativo certo.

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

@mdcfrancis Claro. Mas isso transforma a interpretação da composição que delineei de cabeça para baixo. Ou seja, sin∘x seria equivalente a x |> sin , não?

PS: Talvez tenha se perdido na "álgebra", mas apenas permitir funções na construção do array digitado T[x,y,z] tal que f[x,y,z] é [f(x),f(y),f(z)] dá diretamente

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

que é bastante legível e pode ser tratado como sintaxe ..

Isso é inteligente. Mas eu suspeito que se conseguirmos fazer isso funcionar, sin[x...] realmente perde em verbosidade para sin(x) ou sin~x etc.

E a sintaxe [sin xs] ?

Isso é semelhante em sintaxe à compreensão de matriz [sin(x) for x in xs] .

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

Essa seria a ordem inversa do significado atual do encadeamento da função . Não que eu não me importasse de encontrar um uso melhor para aquele operador, mas precisaria de um período de transição.

@mdcfrancis Sim, eu entendo que é isso que você está buscando. O que inverte as coisas (como @tkelman reitera) wrt. a interpretação da composição que descrevi.

Acho que integrar vetorização e encadeamento seria bem legal. Eu me pergunto se as palavras seriam os operadores mais claros.
Algo como:

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

Vários mapas seguidos podem ser combinados automaticamente em um único mapa para melhorar o desempenho. Observe também que estou assumindo que o mapa terá algum tipo de recurso de transmissão automática. Substituir [1, 2] por _ no início pode criar uma função anônima. Observe que estou usando as regras magrittr do R para encadeamento (veja meu post no encadeamento).

Talvez isso esteja começando a parecer mais com um DSL.

Acompanho essa questão há muito tempo e não comentei até agora, mas isso está começando a ficar fora de controle IMHO.

Eu apoio fortemente a ideia de uma sintaxe limpa para map. Eu gosto mais da sugestão do @tkelman de ~ , pois ele mantém dentro do ASCII para essa funcionalidade básica, e eu gosto bastante de sin~x . Isso permitiria um mapeamento de estilo de uma linha bastante sofisticado, conforme discutido acima. O uso de sin∘x também seria aceitável. Para qualquer coisa mais complicada, costumo pensar que um loop adequado é muito mais claro (e geralmente o melhor desempenho). Eu realmente não gosto muito de transmissão 'mágica', isso torna o código muito mais difícil de seguir. Um loop explícito geralmente é mais claro.

Isso não quer dizer que essa funcionalidade não deva ser adicionada, mas vamos ter uma sintaxe map bem concisa primeiro, especialmente porque está prestes a ficar super rápido (dos meus testes do ramo jb/functions ) .

Observe que um dos efeitos de jb/functions é que broadcast(op, x, y) tem um desempenho tão bom quanto a versão customizada x .op y que especializou manualmente a transmissão em op .

Para qualquer coisa mais complicada, costumo pensar que um loop adequado é muito mais claro (e geralmente o melhor desempenho). Eu realmente não gosto muito de transmissão 'mágica', isso torna o código muito mais difícil de seguir. Um loop explícito geralmente é mais claro.

Eu não concordo. exp(2 * x.^2) é perfeitamente legível e menos detalhado que [exp(2 * v^2) for v in x] . O desafio aqui IMHO é evitar prender as pessoas deixando-as usar o primeiro (que aloca cópias e não funde operações): para isso, precisamos encontrar uma sintaxe que seja curta o suficiente para que a forma lenta possa ser preterida.

Mais pensamentos. Há várias coisas possíveis que você pode querer fazer ao chamar uma função:

loop sem argumentos (cadeia)
percorrer apenas o argumento encadeado (mapa)
percorrer todos os argumentos (mapall)

Cada um dos itens acima pode ser modificado, por:
Marcando um item para percorrer (~)
Marcando um item para não ser repetido (um conjunto extra de [ ] )

Itens uniteráveis ​​devem ser tratados automaticamente, desconsiderando a sintaxe.
A expansão das dimensões singleton deve ocorrer automaticamente se houver pelo menos dois argumentos em loop

A transmissão só faz diferença quando haveria uma dimensão
incompatibilidade caso contrário. Então, quando você diz para não transmitir, você quer dizer
erro se o tamanho do argumento não corresponder?

sin[x...] realmente perde em verbosidade para sin(x) ou sin~x etc.

Além disso, continuando o pensamento, o mapa sin[x...] é uma versão menos ansiosa em [f(x...)] .
A sintaxe

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

ou algo semelhante como [exp(2 * (x..)^2)] estaria disponível e auto-explicativo se alguma vez o encadeamento de função tácita real for introduzido.

@nalimilan sim, mas isso se encaixa na minha categoria 'one-liner' que eu disse que estava bem sem um loop.

Enquanto estamos listando todos os nossos desejos: muito mais importante para mim seria que os resultados de map fossem atribuíveis sem alocação ou cópia. Essa é outra razão pela qual ainda prefiro loops para código crítico de desempenho, mas se isso puder ser mitigado (o nº 249 não está parecendo um ATM esperançoso), tudo isso se tornará muito mais atraente.

resultados do mapa a serem atribuídos sem alocação ou cópia

Você pode expandir isso um pouco? Você certamente pode alterar o resultado de map .

Presumo que ele queira armazenar a saída de map em um array pré-alocado.

Sim, exatamente. Peço desculpas se isso já for possível.

Ah, claro. Temos map! , mas como você observa, #249 está pedindo uma maneira melhor de fazer isso.

@jtravs Eu propus uma solução acima com LazyArray (https://github.com/JuliaLang/julia/issues/8450#issuecomment-65106563), mas até agora o desempenho não foi o ideal.

@toivoh Fiz várias edições nesse post depois de postá-lo. A questão com a qual eu estava preocupado é como descobrir quais argumentos percorrer e quais argumentos não (para que o mapall possa ser mais claro do que broadcast). Acho que se você estiver percorrendo mais de um argumento, a expansão das dimensões singleton para produzir matrizes comparáveis ​​deve sempre ser feita, se necessário, eu acho.

Sim map! está exatamente certo. Seria bom se algum açúcar de sintaxe legal funcionasse aqui também cobrisse esse caso. Não poderíamos ter x := ... mapeando implicitamente o RHS em x .

Eu coloquei um pacote chamado ChainMap que integra mapeamento e encadeamento.

Aqui está um pequeno exemplo:

<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

Continuei pensando sobre isso e acho que finalmente descobri uma sintaxe consistente e Juliana para mapear arrays derivados da compreensão de arrays. A proposta a seguir é juliana porque se baseia na linguagem de compreensão de matrizes já estabelecida.

  1. A partir de f[a...] que foi proposto por @Jutho , a convenção
    que para os vetores a, b
f[a...] == map(f, a[:])
f[a..., b...] == map(f, a[:], b[:])
etc

que não introduz novos símbolos.

2.) Além disso, eu proporia a introdução de um operador adicional: um operador de _preservação de forma_ splatting .. (digamos). Isso ocorre porque ... é um operador _flat_ spatting, então f[a...] deve retornar um vetor e não um array mesmo se a for n -dimensional. Se .. for escolhido, então neste contexto,

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

e o resultado herda a forma dos argumentos. Permitindo transmissão

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

permitiria escrever pensa como

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

Heureka?

Isso não ajuda com expressões de mapeamento, não é? Uma das vantagens da sintaxe over é como ela funciona com expressões:

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

Bem, possivelmente via [sin(x.. * y..)] ou sin[x.. * y..] acima, se você quiser permitir isso. Eu gosto disso um pouco mais do que a sintaxe over porque dá uma dica visual de que os operadores de função nos elementos e não nos contêineres.

Mas você não pode simplificar isso para que x.. simplesmente mapeie x ? Então o exemplo do @johansigfrids seria:

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

@jtravs Por causa do escopo ( [println(g(x..))] vs println([g(x..)]) ) e consistência [x..] = x .

Outra possibilidade é tomar x.. = x[:, 1], x[:, 2], etc. como splat parcial dos subarrays principais (colunas) e ..y como splat parcial dos subarrays finais ..y = y[1,:], y[2,:] . Se ambos passarem por índices diferentes, isso abrange muitos casos interessantes

[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 um vetor, A uma matriz)

Eu prefiro over , pois permite que você escreva uma expressão em sintaxe normal em vez de introduzir muitos colchetes e pontos.

Você está certo sobre a desordem, eu acho e tentei adaptar e sistematizar minha proposta. Para não sobrecarregar ainda mais a paciência de todos, escrevi meus pensamentos sobre mapas e índices etc. em uma essência https://gist.github.com/mschauer/b04e000e9d0963e40058 .

Depois de ler este tópico, minha preferência até agora seria ter _both_ f.(x) para coisas simples e pessoas acostumadas a funções vetorizadas (o idioma " . = vetorizado" é bastante comum), e f(x^2)-x over x para expressões mais complicadas.

Existem muitas pessoas vindo do Matlab, Numpy, etc, para abandonar completamente a sintaxe de função vetorizada; dizer-lhes para adicionar pontos é fácil de lembrar. Uma boa sintaxe do tipo over para vetorizar expressões complexas em um único loop também é muito útil.

A sintaxe over realmente me incomoda. Só me ocorreu o porquê: presume que todos os usos de cada variável em uma expressão são vetorizados ou não vetorizados, o que pode não ser o caso. Por exemplo, log(A) .- sum(A,1) – suponha que removemos a vetorização de log . Você também não pode vetorizar funções sobre expressões, o que parece ser uma falha bastante importante e se eu quisesse escrever exp(log(A) .- sum(A,1)) e ter o exp e o log vetorizados e o sum não?

@StefanKarpinski , então você deve fazer exp.(log.(A) .- sum(A,1)) e aceitar os temporários extras (por exemplo, em uso interativo onde o desempenho não é crítico), ou s = sum(A, 1); exp(log(A) - s) over A (embora isso não esteja certo se sum(A,1) é um vetor e você queria broadcast); você pode apenas ter que usar uma compreensão. Não importa a sintaxe que encontrarmos, não vamos cobrir todos os casos possíveis, e seu exemplo é particularmente problemático porque qualquer sintaxe "automatizada" teria que saber que sum é puro e que pode ser içado para fora do loop/mapa.

Para mim, a primeira prioridade é uma sintaxe f.(x...) para broadcast(f, x...) ou map(f, x...) para que possamos nos livrar de @vectorize . Depois disso, podemos continuar trabalhando em uma sintaxe como over (ou qualquer outra coisa) para abreviar usos mais gerais de map e compreensão.

@stevengj Eu não acho que o segundo exemplo funcione, porque o - não será transmitido . Assumindo que A é uma matriz, a saída seria uma matriz de matrizes de linha única, cada uma das quais é o logaritmo de um elemento de A menos o vetor de somas ao longo da primeira dimensão. Você precisaria broadcast((x, y)->exp(log(x)-y), A, sum(A, 1)) . Mas acho que ter uma sintaxe concisa para map é útil e não precisa necessariamente ser uma sintaxe concisa para broadcast também.

As funções que historicamente foram vetorizadas automaticamente como sin continuarão assim com a nova sintaxe ou isso se tornaria obsoleto? Eu me preocupo que mesmo a sintaxe f. pareça uma 'pegadinha' para uma grande quantidade de programadores científicos que não são motivados por argumentos de elegância conceitual.

Meu sentimento é que as funções historicamente vetorizadas como sin devem ser preteridas em favor de sin. , mas devem ser preteridas quase permanentemente (em vez de serem removidas inteiramente na versão subsequente) para o benefício dos usuários de outras linguagens científicas.

Um problema menor(?) com f.(args...) : embora a sintaxe object.(field) seja na maioria das vezes raramente usada e provavelmente possa ser substituída por getfield(object, field) sem muita dor, há uma _lot_ de definições/referências de método no formato Base.(:+)(....) = .... , e seria doloroso alterá-las para getfield .

Uma solução seria:

  • Faça Base.(:+) se transformar em map(Base, :+) como todos os outros f.(args...) , mas defina um método obsoleto map(m::Module, s::Symbol) = getfield(m, s) para compatibilidade com versões anteriores
  • suporte a sintaxe Base.:+ (que atualmente falha) e recomende isso no aviso de descontinuação para Base.(:+)

Eu gostaria de perguntar novamente - se isso é algo que podemos fazer em 0.5.0? Eu acho que é importante por causa da depreciação de muitos construtores vetorizados. Achei que ficaria bem com isso, mas acho map(Int32, a) , em vez de int32(a) um pouco tedioso.

Isso é basicamente apenas uma questão de escolher a sintaxe neste momento?

Isso é basicamente apenas uma questão de escolher a sintaxe neste momento?

Acho que @stevengj deu bons argumentos a favor de escrever sin.(x) em vez de .sin(x) em seu PR https://github.com/JuliaLang/julia/pull/15032. Então eu diria que o caminho foi liberado.

Eu tinha uma ressalva sobre o fato de que ainda não temos uma solução para generalizar essa sintaxe para expressões compostas de forma eficiente. Mas acho que neste estágio é melhor mesclar esse recurso que cobre a maioria dos casos de uso, em vez de manter essa discussão sem solução indefinidamente.

@JeffBezanson Estou revertendo o marco para 0.5.0 para trazê-lo à tona durante uma discussão de triagem - principalmente para garantir que não me esqueça.

O #15032 também funciona para call - por exemplo Int32.(x) ?

@ViralBShah , sim. Qualquer f.(x...) é transformado em map(f, broadcast, x...) no nível de sintaxe, independentemente do tipo de f .

Essa é a principal vantagem de . sobre algo como f[x...] , que de outra forma é atraente (e não exigiria alterações no analisador), mas funcionaria apenas para f::Function . f[x...] também entra em conflito conceitualmente com as compreensões de array T[...] . Embora eu ache que @StefanKarpinski gosta da sintaxe de colchetes?

(Para escolher outro exemplo, objetos o::PyObject em PyCall podem ser chamados, invocando o método __call__ do objeto Python o , mas os mesmos objetos também podem suportar o[...] indexação f[x...] , mas funcionaria bem com a transmissão o.(x...) .)

call não existe mais.

(Também gosto do argumento de @nalimilan de que f.(x...) torna .( o análogo de .+ etc.)

Sim, a analogia sábia é a que eu mais gosto também. Podemos ir em frente e fundir?

O getfield com um módulo deve ser uma depreciação real?

@tkelman , em oposição a quê? No entanto, o aviso de depreciação para Base.(:+) (isto é, argumentos de símbolos literais) deve sugerir Base.:+ , não getfield . (_Update_: exigia uma depreciação de sintaxe também para lidar com definições de métodos.)

@ViralBShah , houve alguma decisão sobre isso na discussão de triagem de quinta-feira? #15032 está em boa forma para a fusão, eu acho.

Acho que Viral perdeu essa parte da ligação. Minha impressão é que várias pessoas ainda têm reservas sobre a estética de f.(x) e podem preferir

  1. um operador infixo que seria mais simples conceitualmente e na implementação, mas não temos nenhum ascii disponível pelo que posso ver. Minha ideia anterior de depreciar a análise macro de ~ exigiria trabalho para substituir os pacotes e provavelmente é tarde demais para tentar fazer isso neste ciclo.
  2. ou uma nova sintaxe alternativa que facilita a fusão de loops e a eliminação de temporários. Nenhuma outra alternativa foi implementada em qualquer lugar próximo ao nível de #15032, então parece que devemos mesclar isso e experimentá-lo, apesar das reservas restantes.

Sim, eu tenho algumas reservas, mas não consigo ver uma opção melhor do que f.(x) agora. Parece melhor do que escolher um símbolo arbitrário como ~ , e aposto que muitos que estão acostumados com .* (etc.) podem até adivinhar imediatamente o que isso significa.

Uma coisa que eu gostaria de ter uma noção melhor é se as pessoas concordam em _substituir_ definições vetorizadas existentes com .( . Se as pessoas não gostarem o suficiente para fazer a substituição, eu hesitaria mais.

Como um usuário à espreita nesta discussão, gostaria MUITO de usar isso para substituir meu código vetorizado existente.

Eu uso amplamente a vetorização em julia para facilitar a leitura, pois os loops são rápidos. Então eu gosto muito de usá-lo para exp, sin, etc, como foi mencionado anteriormente. Como já vou usar .^, .* em tais expressões adicionando o ponto extra ao sin. exp. etc parece realmente natural, e ainda mais explícito, para mim ... especialmente quando posso facilmente dobrar minhas próprias funções com a notação geral em vez de misturar sin(x) e map(f, x).

Tudo para dizer, como usuário regular, eu realmente espero que isso seja mesclado!

Eu gosto mais da sintaxe fun[vec] proposta do que fun.(vec) .
O que você acha de [fun vec] ? É como uma compreensão de lista, mas com uma variável implícita. Poderia permitir fazer T[fun vec]

Essa sintaxe é gratuita em Julia 0.4 para vetores com comprimento > 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

Algo como [fun over vec] pode ser transformado em nível de sintaxe e talvez valha a pena simplificar [fun(x) for x in vec] mas não é mais simples que map(fun,vec) .

Sintaxes semelhantes a [fun vec] : A sintaxe (fun vec) é gratuita e {fun vec} foi preterida.

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] foi descartado porque entra em conflito com T[vec] . (fun vec) é basicamente a sintaxe Scheme, com o caso de múltiplos argumentos sendo presumivelmente (fun vec1 vec2 ...) ... isso é bem diferente de qualquer outra sintaxe Julia. Ou você pretendia (fun vec1, vec2, ...) , que entra em conflito com a sintaxe da tupla? Também não está claro qual seria a vantagem sobre fun.(vecs...) .

Além disso, lembre-se que um objetivo principal é eventualmente ter uma sintaxe para substituir as funções @vectorized (para que não tenhamos um subconjunto "abençoado" de funções que "funcionem em vetores"), e isso significa que o a sintaxe precisa ser palatável/intuitiva/conveniente para pessoas acostumadas a funções vetorizadas em Matlab, Numpy, etc. Ele também precisa ser facilmente composto para expressões como sin(A .+ cos(B[:,1])) . Esses requisitos excluem muitas das propostas mais "criativas".

sin.(A .+ cos.(B[:,1])) não parece tão ruim, afinal. Isso vai precisar de uma boa documentação. f.(x) será documentado como .( semelhante a .+ ?
.+ pode ser preterido em favor de +. ?

# 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 já inclui documentação, mas quaisquer sugestões adicionais são bem-vindas.

.+ continuará a ser escrito .+ . Primeiro, esse posicionamento do ponto é muito arraigado e não há o suficiente para ganhar alterando a ortografia aqui. Segundo, como @nalimilan apontou, você pode pensar em .( como um "operador de chamada de função vetorizada" e, dessa perspectiva, a sintaxe já é consistente.

(Uma vez que as dificuldades com a computação de tipos em broadcast (#4883) sejam resolvidas, minha esperança é fazer outro PR para que a .⧆ b para qualquer operador seja apenas açúcar para uma chamada para broadcast(⧆, a, b) . Dessa forma, não precisaremos mais implementar .+ etcetera explicitamente — você obterá o operador de transmissão automaticamente apenas definindo + etc. ainda ser capaz de implementar métodos especializados, por exemplo, chamadas para BLAS, sobrecarregando broadcast para operadores específicos.)

Ele também precisa ser facilmente composto para expressões como sin(A .+ cos(B[:,1])) .

É possível analisar f1.(x, f2.(y .+ z)) como broadcast((a, b, c)->(f1(a, f2(b + c))), x, y, z) ?

Edit: vejo que já está mencionado acima ... no comentário oculto por padrão pelo @github ..

@yuyichao , a fusão de loop parece ser possível se as funções estiverem marcadas como @pure (pelo menos se os eltypes forem imutáveis), como comentei em #15032, mas isso é uma tarefa para o compilador, não o analisador. (Mas uma sintaxe vetorizada como essa é mais por conveniência do que para espremer o último ciclo de loops internos críticos.)

Lembre-se que o objetivo principal aqui é eliminar a necessidade de funções @vectorized ; isso requer uma sintaxe pelo menos tão geral, quase tão conveniente e pelo menos tão rápida. Ele não requer fusão de loop automatizada, embora seja bom expor a intenção broadcast do usuário ao compilador para abrir a possibilidade de fusão de loop em alguma data futura.

Existe alguma desvantagem se ele também fizer fusão de loop?

@yuyichao , a fusão de loops é um problema muito mais difícil, e nem sempre é possível deixar de lado funções não puras (por exemplo, veja o exemplo exp(log(A) .- sum(A,1)) de @StefanKarpinski acima). Esperar que isso seja implementado provavelmente resultará em _nunca_ ser implementado, na minha opinião – temos que fazer isso de forma incremental. Comece expondo a intenção do usuário. Se pudermos otimizar ainda mais no futuro, ótimo. Caso contrário, ainda temos um substituto generalizado para o punhado de funções "vetorizadas" disponíveis agora.

Outro obstáculo é que .+ etc. não está atualmente exposto ao analisador como uma operação broadcast ; .+ é apenas outra função. Meu plano é mudar isso (fazer .+ açúcar por broadcast(+, ...) ), conforme observado acima. Mas, novamente, é muito mais fácil progredir se as mudanças forem incrementais.

O que quero dizer é que fazer a fusão de loops provando que isso é válido é difícil, então podemos deixar o analisador fazer a transformação como parte dos esquemas. No exemplo acima, pode ser escrito como. exp.(log.(A) .- sum(A,1)) e ser analisado como broadcast((x, y)->exp(log(x) - y), A, sum(A, 1)) .

Também é bom se .+ ainda não pertencer à mesma categoria (assim como qualquer chamada de função não boardcasted será colocada no argumento) e tudo bem se apenas fizermos isso (fusão de loop) em um versão posterior. Estou perguntando principalmente se é possível ter esse esquema (ou seja, não ambíguo) no analisador e se há alguma desvantagem ao permitir o loop vetorizado e fuzed escrito dessa maneira.

fazer a fusão de loops provando que isso é válido é difícil

Quero dizer, fazer isso no compilador é difícil (talvez não impossível), especialmente porque o compilador precisa examinar a implementação complicada de broadcast , a menos que tenhamos um caso especial broadcast no compilador, que é provavelmente uma má ideia e devemos evitá-la se possível...

Pode ser? É uma ideia interessante, e não parece impossível definir a sintaxe .( como "fusão" dessa maneira, e deixar para o chamador não usá-la para funções impuras. A melhor coisa seria tentar e ver se há algum caso difícil (não estou vendo nenhum problema óbvio no momento), mas estou inclinado a fazer isso após o PR "sem fusão".

Estou inclinado a fazer isso após o PR "sem fusão".

Concordo totalmente, especialmente porque .+ não é tratado de qualquer maneira.

Não quero atrapalhar isso, mas a sugestão de @yuyichao me deu algumas ideias. A ênfase aqui é em quais funções são vetorizadas, mas isso sempre me parece um pouco equivocado – a verdadeira questão é sobre quais variáveis ​​vetorizar, o que determina completamente a forma do resultado. É por isso que estou inclinado a marcar argumentos para vetorização, em vez de marcar funções para vetorização. A marcação de argumentos também permite funções que vetorizam sobre um argumento, mas não sobre outro. Dito isto, podemos ter ambos e este PR serve ao propósito imediato de substituir as funções vetorizadas embutidas.

@StefanKarpinski , quando você chama f.(args...) ou broadcast(f, args...) , ele vetoriza sobre _todos_ os argumentos. (Para esse propósito, lembre-se de que os escalares são tratados como matrizes de dimensão 0.) Na sugestão de @yuyichao de f.(args...) = _sintaxe de transmissão fundida_ (que estou gostando cada vez mais), acho que a fusão seria " stop" em qualquer expressão que não seja func.(args...) (para incluir .+ etc. no futuro).

Assim, por exemplo, sin.(x .+ cos.(x .^ sum(x.^2))) se transformaria (em julia-syntax.scm ) em broadcast((x, _s_) -> sin(x + cos(x^_s_)), x, sum(broacast(^, x, 2))) . Observe que a função sum seria um "limite de fusão". O chamador seria responsável por não usar f.(args...) nos casos em que a fusão estragasse os efeitos colaterais.

Você tem um exemplo em mente onde isso não seria suficiente?

que estou gostando cada vez mais

Estou feliz que você gosta. =)

Apenas mais uma extensão que provavelmente não pertence à mesma rodada, pode ser possível usar .= , .*= ou similar para resolver o problema de atribuição no local (tornando-o distinto do atribuição normal)

Sim, a falta de fusão para outras operações foi minha principal objeção a .+= etcetera em #7052, mas acho que isso seria resolvido tendo .= fusível com outras chamadas func.(args...) . Ou apenas fundir x[:] = ... .

:thumbsup: Existem dois conceitos reunidos nesta discussão que são de fato bastante ortogonais:
as "operações de transmissão fundidas" do matlab'y ou x .* y .+ z e apl'y "mapas em produtos e zips" como f[product(I,J)...] e f[zip(I,J)...] . A conversa entre eles pode ter a ver com isso também.

@mschauer , f.(I, J) já é (em #15032) equivalente a map(x -> f(x...), zip(I, J) se I e J tiverem a mesma forma. E se I for um vetor de linha e J for um vetor de coluna ou vice-versa, então broadcast mapeia o conjunto de produtos (ou você pode fazer f.(I, J') se ambos forem matrizes 1d). Portanto, não entendo por que você acha que os conceitos são "bastante ortogonais".

Ortogonal não era a palavra certa, eles são apenas diferentes o suficiente para coexistir.

O ponto é, porém, que não precisamos de sintaxes separadas para os dois casos. func.(args...) pode suportar ambos.

Assim que um membro do triunvirato (Stefan, Jeff, Viral) mesclar #15032 (que acho que está pronto para mesclagem), fecharei isso e enviarei um problema de roteiro para descrever as alterações propostas restantes: corrigir computação de tipo de transmissão, descontinuar @vectorize , transformar .op em açúcar de transmissão, adicionar "fusão de transmissão" no nível de sintaxe e, finalmente, fundir com atribuição no local. Os dois últimos provavelmente não chegarão ao 0,5.

Ei, estou muito feliz e agradecido pelo 15032. No entanto, eu não descartaria a discussão. Por exemplo, vetores de vetores e objetos semelhantes ainda são muito difíceis de usar em julia, mas podem brotar como ervas daninhas como resultados de compreensões. Uma boa notação implícita não baseada na iteração de codificação em dimensões singleton tem o potencial de facilitar muito isso, por exemplo, com os iteradores flexíveis e novas expressões geradoras.

Acho que isso pode ser fechado agora em favor do #16285.

Esta página foi útil?
0 / 5 - 0 avaliações