Julia: Encadeamento de funções

Criado em 27 jan. 2014  ·  232Comentários  ·  Fonte: JuliaLang/julia

Seria possível permitir a chamada de qualquer função em Any para que o valor seja passado para a função como o primeiro parâmetro e os parâmetros passados ​​para a chamada de função no valor sejam adicionados posteriormente?
ex.

sum(a::Int, b::Int) -> a + b

a = 1
sum(1, 2) # = 3
a.sum(2) # = 3 or
1.sum(2) # = 3

É possível indicar de forma determinística o que uma função retornará para evitar exceções em tempo de execução?

Comentários muito úteis

Nossa lista atual de vários esforços, etc.
Acho que vale a pena que as pessoas verifiquem isso (de preferência antes de opinar, mas w / e)
eles são ligeiramente diferentes.
(Estou tentando fazer o pedido cronologicamente).

Pacotes

Protótipos de não-embalagem

Relacionado:


Talvez isso deva ser editado em uma das principais postagens.

atualizado: 20-04-2020

Todos 232 comentários

A sintaxe . é muito útil, então não vamos torná-la apenas um sinônimo para chamada de função. Não entendo a vantagem de 1.sum(2) sobre sum(1,2) . Para mim, parece confundir as coisas.

A questão sobre exceções é um problema separado? Acho que a resposta é não, além de envolver um corpo de função em try..catch.

O exemplo 1.sum (2) é trivial (eu também prefiro sum (1,2)), mas serve apenas para demonstrar que uma função não pertence per se a esse tipo ex. 1 pode ser passado para uma função com o primeiro parâmetro sendo um Real, não apenas para funções que esperam que o primeiro parâmetro seja um Int.

Edit: Eu posso ter entendido mal o seu comentário. As funções de ponto serão úteis ao aplicar certos padrões de design, como o padrão do construtor comumente usado para configuração. ex.

validate_for(name).required().gt(3) 
# vs 
gt(required(validate_for(name)), 3) 

As exceções às quais estava apenas me referindo são devido a funções que retornam resultados não determinísticos (o que, de qualquer forma, é uma prática ruim). Um exemplo seria chamar a.sum (2) .sum (4) onde .sum (2) às vezes retorna uma String em vez de um Int, mas .sum (4) espera um Int. Acho que o compilador / tempo de execução já é inteligente o suficiente para avaliar tais circunstâncias - que seriam as mesmas ao aninhar a função sum (sum (1, 2), 4) - mas a solicitação de recurso exigiria estender essa funcionalidade para impor restrições de tipo em funções de ponto.

Um dos casos de uso que as pessoas parecem gostar é a "interface fluente". Às vezes é bom em APIs OOP quando os métodos retornam o objeto, então você pode fazer coisas como some_obj.move(4, 5).scale(10).display()

Para mim, acho que isso é melhor expresso como composição de função, mas |> não funciona com argumentos a menos que você use anon. funções, por exemplo, some_obj |> x -> move(x, 4, 5) |> x -> scale(x, 10) |> display , que é muito feio.

Uma opção para suportar esse tipo de coisa seria se |> empurrasse o LHS como o primeiro argumento para o RHS antes de avaliar, mas então ele não poderia ser implementado como uma função simples como é agora.

Outra opção seria algum tipo de macro @composed que adicionaria este tipo de comportamento à seguinte expressão

Você também pode transferir a responsabilidade de apoiar isso para designers de biblioteca, onde eles podem definir

function move(obj, x, y)
    # move the object
end

move(x, y) = obj -> move(obj, x, y)

então, quando você não fornece um objeto, ele faz a aplicação parcial da função (retornando uma função de 1 argumento) que você poderia usar dentro de uma cadeia normal |> .

Na verdade, a definição de |> provavelmente poderia ser alterada agora para o
comportamento que você está pedindo. Eu aceitaria.

Na segunda-feira, 27 de janeiro de 2014, Spencer Russell [email protected]
escrevi:

Um dos casos de uso que as pessoas parecem gostar é a "interface fluente". Está
às vezes bom em APIs OOP quando os métodos retornam o objeto, então você pode fazer
coisas como some_obj.move (4, 5) .scale (10) .display ()

Para mim, acho que isso é melhor expresso como composição de funções, mas
o |> não funciona com argumentos a menos que você use anon. funções, por exemplo, some_obj
|> x -> mover (x, 4, 5) |> x -> escala (x, 10) |> exibição, que é bonita
feio.

Uma opção para apoiar esse tipo de coisa seria se |> empurrasse o LHS como
o primeiro argumento para o RHS antes de avaliar, mas então não poderia ser
implementado como uma função simples como é agora.

Outra opção seria algum tipo de macro @composta que adicionaria este
tipo de comportamento para a seguinte expressão

Você também pode transferir a responsabilidade de apoiar isso para a biblioteca
designers, onde eles poderiam definir

movimento de função (obj, x, y)
# move o objeto
fim

mover (x, y) = obj -> mover (obj, x, y)

então, quando você não fornece um objeto, ele faz um aplicativo de função parcial
(retornando uma função de 1 argumento) que você pode usar dentro de um
normal |> corrente.

-
Responda diretamente a este e-mail ou visualize-o em Gi tHubhttps: //github.com/JuliaLang/julia/issues/5571#issuecomment -33408448
.

ssfrr Gosto da sua maneira de pensar! Não sabia a composição da função |> . Vejo que recentemente houve uma discussão semelhante [https://github.com/JuliaLang/julia/issues/4963].

kmsquire Gosto da ideia de estender a composição da função atual para permitir que você especifique parâmetros na função de chamada ex. some_obj |> move(4, 5) |> scale(10) |> display . O suporte nativo significaria um encerramento a menos, mas o que o ssfrr sugeriu é uma forma viável por enquanto e como um benefício adicional, ele também deve ser compatível com a funcionalidade de composição de função estendida se for implementado.

Obrigado pelas respostas imediatas :)

Na verdade, @ssfrr estava correto - não é possível implementar isso como uma função simples.

O que você quer são macros de threading (ex. Http://clojuredocs.org/clojure_core/clojure.core/-%3E). É uma pena que @ -> @ - >> @ -? >> não seja uma sintaxe viável em Julia.

Sim, eu estava pensando que macros infixas seriam uma forma de implementar isso. Não estou familiarizado o suficiente com macros para saber quais são as limitações.

Acho que isso funciona para a macro de composição de @ssfrr :

Edit: Isso pode ser um pouco mais claro:

import Base.Meta.isexpr
_ispossiblefn(x) = isa(x, Symbol) || isexpr(x, :call)

function _compose(x)
    if !isa(x, Expr)
        x
    elseif isexpr(x, :call) &&    #
        x.args[1] == :(|>) &&     # check for `expr |> fn`
        length(x.args) == 3 &&    # ==> (|>)(expr, fn)
        _ispossiblefn(x.args[3])  #

        f = _compose(x.args[3])
        arg = _compose(x.args[2])
        if isa(f, Symbol)
            Expr(:call, f, arg) 
        else
            insert!(f.args, 2, arg)
            f
        end
    else
        Expr(x.head, [_compose(y) for y in x.args]...)
    end
end

macro compose(x)
    _compose(x)
end
julia> macroexpand(:(<strong i="11">@compose</strong> x |> f |> g(1) |> h('a',"B",d |> c(fred |> names))))
:(h(g(f(x),1),'a',"B",c(d,names(fred))))

Se quisermos ter essa sintaxe |> , eu certamente gostaria de torná-la mais útil do que é agora. Usar apenas para permitir colocar a função a ser aplicada à direita em vez de à esquerda sempre pareceu um desperdício colossal de sintaxe.

+1. É especialmente importante quando você está usando Julia para análise de dados, onde normalmente há pipelines de transformação de dados. Em particular, o Pandas em Python é conveniente de usar porque você pode escrever coisas como df.groupby ("algo"). Aggregate (sum) .std (). Reset_index (), que é um pesadelo de escrever com a sintaxe |> atual .

: +1: para isso.

(Eu já pensei em sugerir o uso do operador .. infixo para isso ( obj..move(4,5)..scale(10)..display ), mas o operador |> também será bom)

Outra possibilidade é adicionar açúcar sintático para currying, como
f(a,~,b) traduzindo para x->f(a,x,b) . Então |> poderia manter seu significado atual.

Oooh, essa seria uma maneira muito boa de transformar qualquer expressão em uma função.

Possivelmente algo como os literais de função anônima de Clojure, onde #(% + 5) é a abreviação de x -> x + 5 . Isso também generaliza para vários argumentos com% 1,% 2, etc., portanto #(myfunc(2, %1, 5, %2) é uma abreviação de x, y -> myfunc(2, x, 5, y)

Esteticamente, não acho que a sintaxe se encaixa muito bem em julia muito legível, mas gosto da ideia geral.

Para usar meu exemplo acima (e mudar para o til de @malmaud em vez de%), você poderia fazer

some_obj |> move(~, 4, 5) |> scale(~, 10) |> display

o que parece muito bom.

Isso é bom porque não dá ao primeiro argumento nenhum tratamento especial. A desvantagem é que usado dessa forma, estamos pegando um símbolo.

Talvez este seja outro lugar onde você poderia usar uma macro, então a substituição só acontece dentro do contexto da macro.

Obviamente, não podemos fazer isso com ~ pois essa já é uma função padrão em Julia. Scala faz isso com _ , o que também poderíamos fazer, mas há um problema significativo em descobrir que parte da expressão é a função anônima. Por exemplo:

map(f(_,a), v)

Qual deles isso significa?

map(f(x->x,a), v)
map(x->f(x,a), v)
x->map(f(x,a), v)

Todas são interpretações válidas. Parece que me lembro que Scala usa as assinaturas de tipo de funções para determinar isso, o que me parece lamentável, pois significa que você não pode realmente analisar Scala sem saber os tipos de tudo. Não queremos fazer isso (e não poderíamos, mesmo que quiséssemos), então deve haver uma regra puramente sintática para determinar qual significado se pretende.

Certo, entendo seu ponto de vista sobre a ambigüidade de até onde ir. Em Clojure, a expressão inteira é envolvida em #(...) portanto, não é ambígua.

Em Julia, é idiomático usar _ como valor irrelevante? Como x, _ = somfunc() se somefunc retorna dois valores e você quer apenas o primeiro?

Para resolver isso, acho que precisaríamos de macro com um uso semelhante a interpolação:

some_obj |> @$(move($, 4, 5)) |> @$(scale($, 10)) |> display

mas, novamente, acho que está ficando muito barulhento nesse ponto, e não acho que @$(move($, 4, 5)) nos forneça algo além da sintaxe existente x -> move(x, 4, 5) , que é IMO mais bonita e mais explícita.

Acho que essa seria uma boa aplicação de uma macro infixo. Como com # 4498, se qualquer regra definir funções como infixo aplicada a macros também, poderíamos ter uma macro @-> ou @|> que teria o comportamento de threading.

Sim, eu gosto da ideia de macro infixo, embora um novo operador pudesse ser introduzido para esse uso em vez de ter um sistema inteiro para macros locais. Por exemplo,
some_obj ||> move($,4,5) ||> scale($, 10) |> disp
ou talvez apenas mantenha |> mas tenha uma regra que
x |> f implicitamente se transforma em x |> f($) :
some_obj |> scale($,10) |> disp

Gente, tudo parece realmente feio: |> ||> etc.
Até agora descobri que a sintaxe de Julia é tão clara que essas coisas discutidas acima não parecem tão bonitas se comparadas a qualquer outra coisa.

Em Scala, é provavelmente a pior coisa - eles têm tantos operadores como ::,:, <<, >> + :: e assim por diante - ele apenas torna qualquer código feio e não legível para alguém sem alguns meses de experiência no uso o idioma.

Lamento saber que você não gosta das propostas, Anton. Seria útil se você fizesse uma proposta alternativa.

Oh, desculpe, não estou tentando ser indelicado. E sim - críticas sem propostas
são inúteis.

Infelizmente, não sou um cientista construindo linguagens, então simplesmente não
saber o que propor ... bem, exceto fazer métodos opcionalmente pertencentes a
objetos como em algumas línguas.

Gosto da frase "cientista construindo linguagens" - soa muito mais grandiosa do que programadores numéricos cansados ​​de Matlab.

Eu sinto que quase toda linguagem tem uma maneira de encadear funções - seja pela aplicação repetida de . em linguagens OO, ou sintaxe especial apenas para esse propósito em linguagens mais funcionais (Haskell, Scala, Mathematica, etc.). Essas últimas linguagens também têm sintaxe especial para argumentos de funções anônimas, mas não acho que Julia realmente vá por aí.

Vou reiterar o apoio à proposta de Spencer - x |> f(a) seja traduzido em f(x, a) , de forma muito análoga a como os blocos de do funcionam (e reforça um tema comum de que o primeiro argumento de um função é privilegiada em Julia para fins de açúcar sintático). x |> f é então visto como mão curta para x |> f() . É simples, não introduz nenhum novo operador, lida com a grande maioria dos casos para os quais queremos o encadeamento de funções, é compatível com versões anteriores e se encaixa nos princípios de design Julia existentes.

Também acho que essa é a melhor proposta aqui, o principal problema é que parece impedir a definição de |> para coisas como redirecionamento de E / S ou outras finalidades personalizadas.

Apenas para observar, . não é uma sintaxe de encadeamento de função especial, mas funciona dessa forma se a função à esquerda retornar o objeto que acabou de modificar, o que é algo que o desenvolvedor da biblioteca tem que fazer intencionalmente.

Analogamente, em Julia um desenvolvedor de biblioteca já pode suportar encadeamento com |> definindo suas funções de N argumentos para retornar uma função de 1 argumento quando dados N-1 argumentos, como mencionado aqui

Isso parece causar problemas se você _quer_ que sua função suporte um número variável de args, portanto, seria bom ter um operador que pudesse realizar o enchimento do argumento.

@JeffBezanson , parece que este operador poderia ser implementado se houvesse uma maneira de fazer macros infixas. Você sabe se há um problema ideológico com isso, ou simplesmente não é implementado?

Recentemente, ~ recebeu uma caixa especial para citar seus argumentos e chamadas
a macro @~ por padrão. |> poderia ser feito para fazer a mesma coisa.

Claro, em alguns meses, alguém vai pedir <| para fazer o mesmo ...

Na quinta-feira, 6 de fevereiro de 2014, Spencer Russell [email protected]
escrevi:

Apenas para observar,. não é uma sintaxe de encadeamento de função especial, mas acontece
funcione dessa forma se a função à esquerda retornar o objeto, ela apenas
modificado, que é algo que o desenvolvedor da biblioteca deve fazer
intencionalmente.

Analogamente, em Julia, um desenvolvedor de biblioteca já pode oferecer suporte ao encadeamento
com |> definindo suas funções de N argumentos para retornar uma função
de 1 argumento quando dados N-1 argumentos, conforme mencionado aquihttps: //github.com/JuliaLang/julia/issues/5571#issuecomment -33408448

Isso parece causar problemas se você _quer_ que sua função ofereça suporte
número variável de args, no entanto, ter um operador que poderia realizar
o argumento de enchimento seria bom.

@JeffBezanson https://github.com/JeffBezanson , parece que este
operador poderia ser implementado se houvesse uma maneira de fazer macros infixas. Você
sabe se há um problema ideológico com isso, ou simplesmente não é implementado?

-
Responda diretamente a este e-mail ou visualize-o em Gi tHubhttps: //github.com/JuliaLang/julia/issues/5571#issuecomment -34374347
.

certo, eu definitivamente não gostaria que este fosse um caso especial. Lidar com isso no design da API não é tão ruim assim, e mesmo a limitação de argumentos variáveis ​​não é um grande problema se você tiver anotações de tipo para eliminar a ambigüidade.

function move(obj::MyType, x, y, args...)
    # do stuff
    obj
end

move(args...) = obj::MyType -> move(obj, args...)

Acho que esse comportamento poderia ser tratado por uma macro @composable que trataria da segunda declaração.

A ideia da macro infixa é atraente para mim na situação em que seria unificada com a declaração de funções infixas, que é discutida em # 4498.

Por que os criadores de Julia são tão contra permitir que os objetos contenham seus próprios métodos? Onde posso ler mais sobre essa decisão? Quais pensamentos e teorias estão por trás dessa decisão?

@meglio um lugar mais útil para perguntas gerais é a lista de discussão ou a tag StackOverflow julia-lang . Veja a conversa de Stefan e os arquivos dos usuários e dev listas de discussões anteriores sobre este tema.

Apenas entrando na conversa, para mim, a coisa mais intuitiva é substituir algum marcador de posição pelo
valor da expressão anterior na sequência de coisas que você está tentando compor, semelhante à macro as-> clojure. Então, é isso:

<strong i="8">@as</strong> _ begin
    3+3
    f(_,y)
    g(_) * h(_,z)
end

seria expandido para:

g(f(3+3,y)) * h(f(3+3,y),z)

Você pode pensar na expressão da linha anterior "caindo" para preencher o buraco de sublinhado na próxima linha.

Comecei a esboçar algo minúsculo como este último trimestre em uma procrastinação da semana de finais.

Também poderíamos oferecer suporte a uma versão oneliner usando |> :

<strong i="19">@as</strong> _ 3+3 |> f(_,y) |> g(_) * h(_,z)

@porterjamesj , gosto dessa ideia!

Concordo; isso é muito bom e tem uma generalidade atraente.
Em 7 de fevereiro de 2014, 15:19, "Kevin Squire" [email protected] escreveu:

@porterjamesj https://github.com/porterjamesj , gosto dessa ideia!

Responda diretamente a este e-mail ou visualize-o em Gi tHubhttps: //github.com/JuliaLang/julia/issues/5571#issuecomment -34497703
.

Eu gosto da ideia de @porterjamesj não apenas porque é uma lufada de ar fresco, mas porque parece muito mais flexível do que as ideias anteriores. Não estamos casados ​​apenas com o uso do primeiro argumento, temos o domínio da escolha da variável intermediária, e isso também parece algo que podemos implementar agora, sem ter que adicionar uma nova sintaxe ou casos especiais à linguagem.

Observe que em Julia, como não fazemos muito do padrão obj.method(args...) e, em vez disso, fazemos o padrão method(obj, args...) , tendemos a não ter métodos que retornam os objetos nos quais operam para o expresso propósito do encadeamento de métodos. (Que é o que jQuery faz, e é fantástico em javascript). Portanto, não economizamos tanta digitação aqui, mas com o propósito de ter "tubos" configurados entre as funções, acho isso muito bom.

Dado que -> e ->> do clojure são apenas casos especiais dos anteriores e bastante comuns, provavelmente poderíamos implementá-los facilmente também. Embora a questão de como chamá-los seja um pouco complicada. Talvez @threadfirst e @threadlast ?

Eu gosto da ideia de ser uma macro também.

Não é melhor se a expansão, seguindo o exemplo, for algo como

tmp = 3+3; tmp = f(tmp); return h(tmp, z)

evitar várias chamadas para a mesma operação? (Talvez isso já estivesse implícito na ideia de @porterjamesj )

Outra sugestão: seria possível que a macro expanda os atalhos f para f(_) e f(y) para f(_,y) ? Talvez seja demais, mas acho que temos a opção de usar o marcador apenas quando necessário ... (os atalhos devem, no entanto, ser permitidos apenas em chamadas de função sozinhas, não em expressões como g(_) * h(_,z) acima)

@cdsousa, o ponto sobre como evitar chamadas múltiplas é bom. A implementação do clojure usa ligações let sequenciais para conseguir isso; Não tenho certeza se podemos escapar impunes, porque não sei o suficiente sobre o desempenho de nossos let .

Então, a macro @as usando quebras de linha e => como pontos de divisão para decidir qual é a expressão de substituição e o que será substituído?

let desempenho é bom; agora pode ser tão rápido quanto uma atribuição de variável quando possível, e também muito rápido caso contrário.

@ssfrr na minha implementação de brinquedo é apenas filtra todos os nós relacionados à quebra de linha que o analisador insere (NB, eu realmente não entendo tudo isso, provavelmente seria bom ter documentação sobre eles no manual) e então reduz a substituição sobre a lista de expressões que permanece. Usar let seria melhor, embora eu acho.

@cdsousa :

Outra sugestão: seria possível que a macro expanda os atalhos f para f(_) e f(y) para f(_,y)

f to f(_) faz sentido para mim. Para o segundo, sou de opinião que especificar explicitamente a localização é melhor, já que pessoas razoáveis ​​poderiam argumentar que f(_,y) ou f(y,_) é mais natural.

Dado que -> e ->> do clojure são apenas casos especiais dos anteriores, e bastante comuns, provavelmente poderíamos implementá-los facilmente também. Embora a questão de como chamá-los seja um pouco complicada. Talvez @threadfirst e @threadlast ?

Acho que especificar o local explicitamente com f(_,y...) ou f(y..., _) permite que o código seja bem compreensível. Embora a sintaxe extra (e os operadores) façam sentido no Clojure, não temos operadores adicionais disponíveis e acho que as macros adicionais geralmente tornariam o código menos claro.

Então, a macro @as usando quebras de linha e => como pontos de divisão para decidir qual é a expressão de substituição e o que será substituído?

Eu acharia mais natural usar |> como um ponto de divisão, uma vez que já é usado para pipelining

Só para você saber, há uma implementação da macro de threading em Lazy.jl , que permite escrever, por exemplo:

@>> range() map(x->x^2) filter(iseven)

No lado positivo, não requer nenhuma alteração de idioma, mas fica um pouco feio se você quiser usar mais de uma linha.

Eu também poderia implementar @as> em Lazy.jl se houver interesse. Lazy.jl agora tem uma macro @as também.

Você também pode fazer algo assim (embora usando uma sintaxe semelhante a Haskell) com Monads.jl (nota: ele precisa ser atualizado para usar a sintaxe Julia atual). Mas eu suspeito que uma versão especializada apenas para discussão de argumentos deve ser capaz de evitar as armadilhas de desempenho que a abordagem geral tem.

Lazy.jl parece um pacote muito bom e é mantido ativamente. Existe um motivo convincente para que isso esteja no Base?

Como o encadeamento de funções funcionará com funções que retornam vários valores?
Qual seria o resultado do encadeamento, por exemplo:

function foo(a,b)
    a+b, a*b   # x,y respectively
end

e bar(x,z,y) = x * z - y ser?

Não exigiria uma sintaxe como bar(_1,z,_2) ?

Jogando em outro exemplo:

data = [2.255, 3.755, 6.888, 7.999, 9.001]

A maneira limpa de escrever: log(sum(round(data))) é data|>round|>sum|>log
Mas se quisermos fazer um log de base 2 e arredondar para 3 decimais,
então: só podemos usar o primeiro formulário:
log(2,sum(round(data,3)))

Mas, idealmente, gostaríamos de ser capazes de fazer:
data|>round(_,3)|>sum|>log(2,_)
(ou similar)

Fiz um protótipo de como sugiro que funcione.
https://github.com/oxinabox/Pipe.jl

Isso não resolve o ponto de @gregid , mas estou trabalhando nisso agora.
Também não lida com a necessidade de expandir os argumentos

É semelhante às macros de threading Lazy.jl de @ one-more-minute, mas mantém o símbolo |> para legibilidade (preferência pessoal).

Eu irei lentamente transformá-lo em um pacote, talvez, em algum momento

Mais uma opção é:

data |>   x -> round(x,2)  |> sum |>  x -> log(2,x)

Embora seja maior que log(2,sum(round(data,2))) essa notação às vezes ajuda na legibilidade.

@shashi isso não é ruim, não pensei nisso,
Eu acho geralmente muito prolixo para ser facilmente legível

https://github.com/oxinabox/Pipe.jl Agora resolve o problema de @gregid .
Porém, se você pedir _[1] e _[2] ele fará isso fazendo várias chamadas para a substituição
O que não estou certo é o comportamento mais desejável.

Como alguém de fora, acho que o operador de duto se beneficiaria em adaptar o tratamento que o F # dá a ele.
Concedido, F # tem currying, mas um pouco de mágica poderia talvez ser feito no back-end para que não exija isso. Como, na implementação do operador, e não a linguagem central.

Isso faria com que [1:10] |> map(e -> e^2) resultasse em [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] .

Olhando para trás, @ssfrr aludiu a isso, mas o argumento obj em seu exemplo seria automaticamente dado a map como o segundo argumento em meu exemplo, evitando assim que os programadores tivessem que definir suas funções para apoie isso.

O que você acha que isso significa?

Em 5 de junho de 2015, às 17:22, H-225 [email protected] escreveu:

Como alguém de fora, acho que uma das melhores maneiras de fazer isso seria adaptar o tratamento que F # dá a isso.
Concedido, F # tem currying, mas um pouco de mágica poderia talvez ser feito no back-end para que não exija isso. Como, na implementação do operador, e não a linguagem central.

Isso faria [1:10] |> map (e -> e ^ 2) resultar em [1, 4, 9, 16, 25, 36, 49, 64, 81, 100].

Pessoalmente, acho que é bom e claro, sem ser muito prolixo.

Obviamente, pode-se escrever result = map (sqr, [1:10]), mas eles por que tem o operador de pipeline?
Talvez haja algo que estou perdendo?

-
Responda a este e-mail diretamente ou visualize-o no GitHub.

@StefanKarpinski
Basicamente, faça com que o operador trabalhe como:

  • x |> y(f) = y(x, f)
  • x |> y(f) = y(f, x)

Talvez tenha um padrão de interface em que qualquer função a ser usada com o operador use os dados para operar como o primeiro ou o último argumento, dependendo de qual dos itens acima for selecionado para ser esse padrão.
Assim, para a função map como exemplo, map seria map(func, data) ou map(data, func) .

Isso está mais claro?

Lazy.jl parece um pacote muito bom e é mantido ativamente. Existe um motivo convincente para que isso esteja no Base?

Acho que esta é a questão importante aqui.

A razão pela qual isso pode ser desejável na base é 2 vezes:

1.) Podemos encorajar o pipelining como sendo o Caminho Juliano - podemos argumentar que é mais legível
2.) coisas como Lazy.jl, FunctionalData.jl e meu próprio Pipe.jl requerem uma macro para envolver a expressão na qual ela deve agir - o que a torna menos legível.

Acho que a resposta pode estar em ter macros do Infix.
E definindo |> como tal.

Eu não tenho certeza se |>, (ou seu primo o bloco do) pertence ao núcleo.
Mas as ferramentas não existem para defini-los fora do analisador.

A capacidade de ter esse tipo de sintaxe de pipeline parece muito boa. Isso poderia ser adicionado ao Base, ou seja, x |> y(f) = y(f, x) part, que Lazy.j, FunctionalData.jl e Pipe.jl poderiam usar? : +1:

Tendo examinado o código que usa as várias implementações disso em pacotes, pessoalmente acho que é ilegível e muito pouco Juliano. O trocadilho do pipeline da esquerda para a direita não ajuda na legibilidade, apenas faz seu código se destacar como reverso do resto do código perfeitamente normal que usa parênteses para avaliação de função. Prefiro desencorajar uma sintaxe que leva a 2 estilos diferentes, em que o código escrito em um dos estilos fica de dentro para fora e para trás em relação ao código escrito no outro. Por que não se contentar com a sintaxe perfeitamente boa que já temos e encorajar fazer as coisas parecerem mais uniformes?

@tkelman
Pessoalmente, vejo isso de um ponto de vista um tanto utilitário.
Concedido, talvez se você estiver fazendo algo simples, então não é necessário, mas se você está escrevendo uma função, digamos, isso faz algo bastante complicado, ou prolixo, (de repente: manipulação de dados, por exemplo) então eu acho que é onde a sintaxe do pipeline brilha.

Eu entendo o que você quer dizer; ela _seria_ mais uniforme se você tivesse uma sintaxe de chamada de função para tudo. Porém, pessoalmente, acho melhor tornar mais fácil escrever um código [complicado] que possa ser facilmente compreendido. Concedido, você tem que aprender a sintaxe e o que ela significa, mas, IMHO, |> não é mais difícil de entender do que como chamar uma função.

@tkelman Eu olharia para isso de um ponto de vista diferente. Obviamente, existem pessoas que preferem esse estilo de programação. Eu posso ver que talvez você queira ter um estilo consistente para o código-fonte do Base, mas isso é apenas sobre a adição do suporte de analisador para seu estilo preferido de programação _seus_ aplicativos Julia. Os julianos realmente querem ditar ou sufocar algo que outras pessoas acham benéfico?
Achei o pipelining muito útil no Unix, então, embora nunca tenha usado uma linguagem de programação que o habilitasse na linguagem, pelo menos daria o benefício da dúvida.

Temos |> como um operador de piping de função, mas existem limitações de implementação para como é feito atualmente que o tornam muito lento no momento.

Piping é ótimo em um shell Unix onde tudo leva o texto dentro e fora do texto. Com tipos mais complicados e várias entradas e saídas, não é tão claro. Portanto, temos duas sintaxes, mas uma faz muito menos sentido no caso MIMO. O suporte do analisador para estilos alternativos de programação ou DSLs geralmente não é necessário, pois temos macros poderosas.

OK, obrigado, estava indo pelo comentário de @oxinabox :

Mas as ferramentas não existem para defini-los fora do analisador.

Está entendido o que seria feito para remover as limitações de implementação às quais você se referiu?

Algumas das sugestões anteriores poderiam potencialmente ser implementadas fazendo |> analisar seus argumentos como uma macro em vez de uma função. O antigo significado de piping de objeto de comando de |> tornou-se obsoleto, portanto, ele pode realmente ser liberado para fazer algo diferente com o 0,5-dev.

No entanto, essa escolha me lembra um pouco da análise especial de ~ que considero um erro pelos motivos que declarei em outro lugar.

Analisar ~ é simplesmente insano, é uma função na base. Usando _ , _1 , _2 , parece _mais_ razoável (especialmente se você aumentar se essas variáveis ​​forem definidas em outro lugar no escopo). Ainda assim, até que tenhamos funções anônimas mais eficientes, parece que não vai funcionar ...

implementado fazendo |> analisar seus argumentos como uma macro em vez de uma função

A menos que você faça isso!

Analisar ~ é simplesmente insano, é uma função de base

É um operador unário para a versão bit a bit. O binário Infix ~ analisa como uma macro, ref https://github.com/JuliaLang/julia/issues/4882 , que eu acho que é um uso estranho de um operador ASCII (https://github.com/ JuliaLang / julia / pull / 11102 # issuecomment-98477891).

@tkelman

Portanto, temos duas sintaxes, mas uma faz muito menos sentido no caso MIMO.

3 sintaxes. Mais ou menos.
Pipe in, chamada de função normal e blocos Do.
Discutível até 4, já que as macros também usam uma convenção diferente.


Para mim,
o Readorder (ou seja, da esquerda para a direita) == A ordem do aplicativo torna, para cadeias de funções SISO, muito mais claras.

Eu faço muitos códigos como (usando iterators.jl e pipe.jl):

  • loaddata(filename) |> filter(s-> 2<=length(s)<=15, _) |> take!(150,_) |> map(eval_embedding, _)
  • results |> get_error_rate(desired_results, _) |> round(_,2)

Para SISO, é melhor (para minha preferência pessoal), para MIMO não é.

Julia parece já ter decidido que existem várias maneiras corretas de fazer as coisas.
O que não tenho 100% de certeza é uma coisa boa.

Como eu disse, gostaria que os blocos Pipe e Do saíssem da linguagem principal.

Os blocos Do têm alguns casos de uso muito úteis, mas me incomodou um pouco que eles tenham que usar a primeira entrada como a função, nem sempre se encaixa perfeitamente com a filosofia de envio múltiplo (e nem o pandas / UFCS estilo D com postfix data.map(f).sum() , eu sei que é popular, mas não acho que pode ser combinado de forma eficaz com envio múltiplo).

O encanamento provavelmente pode ser descontinuado em breve e deixado para os pacotes a serem usados ​​em DSLs como o Pipe.jl.

Julia parece já ter decidido que existem várias maneiras corretas de fazer as coisas.
O que não tenho 100% de certeza é uma coisa boa.

Está relacionado à questão de saber se podemos ou não aplicar rigorosamente um guia de estilo para toda a comunidade. Até agora não fizemos muito aqui, mas para interoperabilidade, consistência e legibilidade de pacotes de longo prazo, acho que isso se tornará cada vez mais importante à medida que a comunidade crescer. Se você é a única pessoa que vai ler seu código, enlouqueça e faça o que quiser. Se não, porém, vale a pena negociar um pouco pior (em sua opinião) legibilidade por uma questão de uniformidade.

@tkelman @oxinabox
Ainda estou para encontrar uma razão clara por que ele não deve ser incluído na linguagem, ou mesmo nos pacotes "principais". [por exemplo: Base]
Pessoalmente, acho que fazer |> uma macro pode ser a resposta.
Algo assim, talvez? (Eu não sou um programador mestre de Julia!)

macro (|>) (x, y::Union(Symbol, Expr))
    if isa(y, Symbol)
        y = Expr(:call, y) # assumes y is callable
    end
    push!(y.args, x)
    return eval(y)
end

Com Julia v0.3.9, não fui capaz de defini-lo duas vezes - uma vez com um símbolo e outra com uma expressão; meu entendimento [limitado] de Union é que há um impacto no desempenho ao usá-lo, então estou supondo que isso seria algo a retificar em meu código de exemplo de brinquedo.

Claro, há um problema com a sintaxe de uso para isso.
Por exemplo, para executar o equivalente a log(2, 10) , você deve escrever @|> 10 log(2) , o que não é desejável aqui.
Meu entendimento é que você teria que ser capaz de de alguma forma marcar funções / macros como "infixáveis", por assim dizer, de forma que você pudesse escrever assim: 10 |> log(2) . (Corrija se estiver errado!)
Exemplo artificial, eu sei. Não consigo pensar em um bom agora! =)

Também vale a pena apontar uma área que não abordei no meu exemplo ...
Então, por exemplo:

julia> for e in ([1:10], [11:20] |> zip) println(e) end
(1,11)
(2,12)
(3,13)
(4,14)
(5,15)
(6,16)
(7,17)
(8,18)
(9,19)
(10,20)

Novamente - exemplo inventado, mas espero que você tenha entendido!
Eu fiz algumas alterações, mas enquanto escrevia isso, eu não conseguia descobrir como implementar isso sozinho.

Em 9 de junho de 2015, às 21h37, H-225 [email protected] escreveu:

Ainda não encontrei uma razão clara por que não deveria ser incluído na linguagem

Essa é a postura mental errada para o projeto de uma linguagem de programação. A pergunta deve ser "por quê?" em vez de "por que não?" Cada recurso precisa de um motivo convincente para sua inclusão e, mesmo com um bom motivo, você deve pensar muito antes de adicionar qualquer coisa. Você pode viver sem ele? Existe uma maneira diferente de realizar a mesma coisa? Existe uma variação diferente do recurso que seria melhor e mais geral ou mais ortogonal aos recursos existentes? Não estou dizendo que essa ideia em particular não poderia acontecer, mas deve haver uma justificativa muito melhor do que "por que não?" com alguns exemplos que não são melhores do que a sintaxe normal.

A pergunta deve ser "por quê?" em vez de "por que não?"

+ 1_000_000

De fato.
Veja esta postagem de blog bastante conhecida:
Cada recurso começa com -100 pontos.
É necessário fazer uma grande melhoria para valer a pena adicionar ao idioma.

FWIW, Pyret (http://www.pyret.org/) passou por essa mesma discussão alguns meses atrás. A linguagem suporta uma notação "bola de canhão" que originalmente funcionava da mesma forma que as pessoas estão propondo com |> . Em Pyret,

[list: 1, 2, 3, 5] ^ map(add-one) ^ filter(is-prime) ^ sum() ^ ...

Portanto, a notação de bala de canhão foi desenvolvida para adicionar argumentos às funções.

Não demorou muito para que eles decidissem que essa sintaxe era muito confusa. Por que sum() está sendo chamado sem nenhum argumento? Em última análise, eles optaram por uma alternativa elegante ao curry:

[list: 1, 2, 3, 5] ^ map(_, add-one) ^ filter(_, is-prime) ^ sum() ^ ...

Isso tem a vantagem de ser mais explícito e simplificar o operador ^ para uma função simples.

Sim, isso parece muito mais razoável para mim. Também é mais flexível do que currying.

@StefanKarpinski Estou um pouco confuso. Você quis dizer mais flexível do que encadeamento (não currying)? Afinal, a solução de Pyret foi simplesmente usar currying, que é mais geral do que encadear.

Talvez, se modificarmos a sintaxe |> um pouco (eu realmente não sei como é difícil de implementar, talvez ela entre em conflito com | e > ), nós poderia definir algo flexível e legível.

Definindo algo como

foo(x,y) = (y,x)
bar(x,y) = x*y

Nós teríamos:

randint(10) |_> log(_,2) |> sum 
(1,2) |_,x>  foo(_,x)   |x,_>   bar(_,2) |_> round(_, 2) |> sum |_> log(_, 2)

Em outras palavras, teríamos um operador como |a,b,c,d> onde a , b , c e d obteria os valores retornados de a última expressão (em ordem) e use-a nos espaços reservados dentro da próxima.

Se não houver variáveis ​​dentro de |> , funcionaria como agora. Também poderíamos definir um novo padrão: f(x) |> g(_, 1) obteria todos os valores retornados por f(x) e associaria ao marcador de posição _ .

@samuela , o que eu quis dizer é que, com currying, você só pode omitir argumentos finais, enquanto com a abordagem _ , você pode omitir quaisquer argumentos e obter uma função anônima. Ou seja, dado f(x,y) com currying, você pode fazer f(x) para obter uma função que faça y -> f(x,y) , mas com sublinhados você pode fazer f(x,_) pela mesma coisa, mas também faça f(_,y) para obter x -> f(x,y) .

Embora eu goste da sintaxe de sublinhado, ainda não estou satisfeito com nenhuma resposta proposta à pergunta de quanto da expressão circundante ela "captura".

o que você faz se uma função retorna vários resultados? Teria que passar uma tupla para a posição _? Ou poderia haver uma sintaxe para dividi-lo na hora? Pode ser uma pergunta estúpida, em caso afirmativo, desculpe!

@StefanKarpinski Ah, entendo o que você quer dizer. Acordado.

@ScottPJones, a resposta óbvia é permitir setas de arte ASCII:
http://scrambledeggsontoast.github.io/2014/09/28/needle-announce/

@simonbyrne Isso parece ainda pior do que programar em Fortran IV em cartões perfurados, como eu fazia na minha juventude desperdiçada! Gostaria de saber se alguma sintaxe como _1, _2, etc. pode permitir separar um retorno múltiplo, ou isso é apenas uma ideia estúpida da minha parte?

@simonbyrne Isso é brilhante. Implementar isso como uma macro de string seria um projeto GSoC incrível.

Por que sum () está sendo chamado sem nenhum argumento?

Eu acho que o argumento implícito também é uma das coisas mais confusas sobre a notação do , então seria bom se pudéssemos utilizar a mesma convenção para isso também (embora eu saiba que é muito mais difícil, pois já está incorporado ao idioma).

@simonbyrne Você não acha que poderia ser feito de uma forma inequívoca? Nesse caso, acho que vale a pena quebrar isso (a notação do atual), se puder ser tornada mais lógica, mais geral e consistente com o encadeamento.

@simonbyrne Sim, concordo totalmente. Eu entendo a motivação para a notação do atual, mas sinto fortemente que ela não justifica a ginástica sintática.

@samuela em relação ao mapa (f, _) vs apenas o mapa (f). Eu concordo que alguma remoção de magia seria confusa, mas eu acho que map (f) é algo que deveria existir. Não seria necessário e o açúcar apenas adicionaria um método simples para mapear.
por exemplo

map(f::Base.Callable) = function(x::Any...) map(f,x...) end

ou seja, o mapa recebe uma função e retorna uma função que funciona em coisas que são iteráveis ​​(mais ou menos).

De maneira mais geral, acho que devemos nos inclinar para funções que têm métodos de "conveniência" adicionais, em vez de algum tipo de convenção de que |> sempre mapeia dados para o primeiro argumento (ou similar).

Na mesma linha, poderia haver um

type Underscore end
_ = Underscore()

e uma convenção geral de que as funções devem / podem ter métodos que usam sublinhados em certos argumentos e, em seguida, retornam funções que usam menos argumentos. Estou menos convencido de que isso seria uma boa ideia, pois seria necessário adicionar 2 ^ n métodos para cada função que leva n argumentos. Mas é uma abordagem. Eu me pergunto se seria possível não ter que adicionar explicitamente tantos métodos, mas sim conectar-se à pesquisa de método, de modo que, se algum argumento for do tipo Sublinhado, a função apropriada seja retornada.

De qualquer forma, eu definitivamente acho que ter uma versão de mapa e filtro que apenas pega um chamável e retorna um chamável faz sentido, a coisa com o sublinhado pode ou não ser viável.

@patrickthebold
Eu imagino que x |> map(f, _) => x |> map(f, Underscore()) => x |> map(f, x) , como você propõe, seria a maneira mais simples de implementar map(f, _) , certo? - apenas _ seja uma entidade especial para a qual você programa?

Porém, não tenho certeza se isso seria melhor do que inferir automaticamente por Julia - presumivelmente usando a sintaxe |> - em vez de ter que programá-la você mesmo.

Além disso, em relação à sua proposta de map - eu meio que gosto. De fato, para os |> atuais, isso seria bastante útil. Porém, eu imagino que seria mais simples melhor apenas implementar a inferência automática de x |> map(f, _) => x |> map(f, x) vez disso?

@StefanKarpinski Faz sentido. Não tinha pensado nisso exatamente assim.

Nada do que eu dissesse estaria vinculado a |> de alguma forma. O que eu quis dizer em relação a _ seria, por exemplo, adicionar métodos a < como tal:

<(_::Underscore, x) = function(z) z < x end
<(x, _::Underscore) = function(z) x < z end

Mas, novamente, acho que isso seria uma dor, a menos que houvesse uma maneira de adicionar automaticamente os métodos apropriados.

Novamente, a coisa com os sublinhados é diferente de adicionar o método de conveniência para mapear conforme descrito acima. Acho que ambos deveriam existir, de uma forma ou de outra.

@patrickthebold Tal abordagem com um tipo definido pelo usuário para sublinhado, etc, colocaria uma carga significativa e desnecessária no programador ao implementar funções. Tendo que listar todos os 2 ^ n de

f(_, x, y) = ...
f(x, _, y) = ...
f(_, _, y) = ...
...

seria muito chato, para não dizer deselegante.

Além disso, sua proposição com map suponho que forneça uma sintaxe alternativa para map(f) com funções básicas como map e filter mas em geral sofre do mesmo questão de complexidade como a abordagem de sublinhado manual. Por exemplo, por func_that_has_a_lot_of_args(a, b, c, d, e) você teria que passar pelo cansativo processo de digitar cada "currying" possível

func_that_has_a_lot_of_args(a, b, c, d, e) = ...
func_that_has_a_lot_of_args(b, c, d, e) = ...
func_that_has_a_lot_of_args(a, b, e) = ...
func_that_has_a_lot_of_args(b, d, e) = ...
func_that_has_a_lot_of_args(a, d) = ...
...

E mesmo que o fizesse, ainda enfrentaria uma quantidade absurda de ambigüidade ao chamar a função: func_that_has_a_lot_of_args(x, y, z) refere à definição em que x=a,y=b,z=c ou x=b,y=d,z=e , etc. ? Julia poderia discernir entre eles com informações de tipo de tempo de execução, mas para o programador leigo lendo o código-fonte, seria totalmente obscuro.

Acho que a melhor maneira de fazer o currying de sublinhado da maneira certa é simplesmente incorporá-lo à linguagem. Afinal, seria uma mudança muito direta para o compilador. Sempre que um sublinhado aparecer em um aplicativo de função, basta puxá-lo para criar um lambda. Comecei a pensar em implementar isso há algumas semanas, mas infelizmente não acho que terei tempo livre suficiente nas próximas semanas para ver isso até o fim. Para alguém familiarizado com o compilador Julia, provavelmente, não demoraria mais do que uma tarde para fazer as coisas funcionarem.

@samuela
Você pode esclarecer o que quer dizer com "puxar para fora para criar um lambda"? - Estou curioso. Eu também me perguntei como isso pode ser implementado.

@patrickthebold
Ah - entendo. Presumivelmente, você poderia usar algo assim: filter(_ < 5, [1:10]) => [1:4] ?
Pessoalmente, acho filter(e -> e < 5, [1:10]) mais fácil de ler; mais consistente - significado menos oculto, embora eu admita, é mais conciso.

A menos que você tenha um exemplo em que realmente brilha?

@samuela

Além disso, sua proposição com map, suponho, forneceria uma sintaxe alternativa para map (f) com funções básicas como map e filter, mas em geral sofre do mesmo problema de complexidade que a abordagem de sublinhado manual.

Eu não estava sugerindo que isso fosse feito em geral, apenas para map e filter , e possivelmente alguns outros lugares onde pareça óbvio. Para mim, é assim que map deve funcionar: pegar uma função e retornar uma função. (tenho certeza que é isso que Haskell faz.)

seria muito chato, para não dizer deselegante.

Acho que estamos de acordo nisso. Eu espero que haja uma maneira de adicionar algo à linguagem para lidar com invocações de método onde alguns argumentos são do tipo Sublinhado. Pensando melhor, acho que tudo se resume a ter um caractere especial que se expande automaticamente para lambda, ou tem um tipo especial que se expande automaticamente para lambda. Eu não me sinto fortemente de qualquer maneira. Posso ver as vantagens e desvantagens de ambas as abordagens.

@ H-225 sim, o sublinhado é apenas uma conveniência sintática. Não tenho certeza de quão comum é, mas Scala certamente tem. Pessoalmente eu gosto, mas acho que é apenas uma dessas coisas de estilo.

@ H-225 Bem, neste caso acho que um exemplo convincente e relevante seria o encadeamento de funções. Em vez de ter que escrever

[1, 2, 3, 5]
  |> x -> map(addone, x)
  |> x -> filter(isprime, x)
  |> sum
  |> x -> 3 * x
  |> ...

alguém poderia simplesmente escrever

[1, 2, 3, 5]
  |> map(addone, _)
  |> filter(isprime, _)
  |> sum
  |> 3 * _
  |> ...

Eu me pego sem saber usando essa sintaxe de sublinhado (ou alguma ligeira variante) constantemente em linguagens que a suportam e só percebo como ela é útil ao fazer a transição para trabalhar em linguagens que não a suportam.

Até onde eu sei, existem atualmente pelo menos 3,5 bibliotecas / abordagens que tentam resolver este problema em Julia: função |> incorporada de Julia, Pipe.jl, Lazy.jl e 0,5 para do incorporada de Julia

@samuela se você gostaria de brincar com uma implementação desta ideia, você poderia experimentar FunctionalData.jl, onde seu exemplo ficaria assim:

<strong i="7">@p</strong> map [1,2,3,4] addone | filter isprime | sum | times 3 _

A última parte mostra como canalizar a entrada para o segundo parâmetro (o padrão é o argumento um, caso em que _ pode ser omitido). Feedback muito apreciado!


Editar: o texto acima é simplesmente reescrito para:

times(3, sum(filter(map([1,2,3,4],addone), isprime)))

que usa FunctionalData.map e filter em vez de Base.map e filter. A principal diferença é a ordem dos argumentos, a segunda diferença é a convenção de indexação (consulte a documentação). Em qualquer caso, Base.map pode simplesmente ser usado invertendo a ordem dos argumentos. @p é uma regra de reescrita bem simples (da esquerda para a direita torna-se interno para externo, além do suporte para currying simples: <strong i="17">@p</strong> map data add 10 | showall torna-se

showall(map(data, x->add(x,10)))

O hack pode apresentar algo assim: https://github.com/facebook/hhvm/issues/6455. Eles estão usando $$ que está fora da mesa para Julia ( $ já está muito sobrecarregado).

FWIW, eu realmente gosto da solução de Hack para isso.

Eu também gosto, minha principal reserva é que eu ainda gostaria de uma notação lambda terser que poderia usar _ para variáveis ​​/ slots e seria bom ter certeza de que eles não entrem em conflito.

Não se poderia usar __ ? Qual é a sintaxe lambda em que você está pensando? _ -> sqrt(_) ?

Claro, poderíamos. Essa sintaxe já funciona, é mais sobre uma sintaxe que não requer a seta, de modo que você pode escrever algo ao longo das linhas de map(_ + 2, v) , o verdadeiro problema é quanto da expressão circundante _ pertence a.

O Mathematica não tem um sistema semelhante para argumentos anônimos? Como
eles lidam com o escopo da delimitação desses argumentos?
Na terça-feira, 3 de novembro de 2015 às 9h09 Stefan Karpinski [email protected]
escrevi:

Claro, poderíamos. Essa sintaxe já funciona, é mais sobre uma sintaxe que
não requer a seta, para que você possa escrever algo ao longo das linhas
do mapa (_ + 2, v), o problema real sendo quanto do entorno
expressão à qual o _ pertence.

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

https://reference.wolfram.com/language/tutorial/PureFunctions.html , mostrando
o símbolo #, é o que eu estava pensando.
Na terça- feira , 3 de novembro de 2015 às 9h34, Jonathan Malmaud

O Mathematica não tem um sistema semelhante para argumentos anônimos? Como
eles lidam com o escopo da delimitação desses argumentos?
Na terça-feira, 3 de novembro de 2015 às 9h09 Stefan Karpinski [email protected]
escrevi:

Claro, poderíamos. Essa sintaxe já funciona, é mais sobre uma sintaxe que
não requer a seta, para que você possa escrever algo ao longo das linhas
do mapa (_ + 2, v), o problema real sendo quanto do entorno
expressão à qual o _ pertence.

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

O Mathematica usa & para delimitá-lo.

Em vez de fazer algo tão geral como uma sintaxe lambda mais curta (que poderia pegar uma expressão arbitrária e retornar uma função anônima), poderíamos contornar o problema do delimitador, confinando as expressões aceitáveis ​​para chamadas de função e as variáveis ​​/ slots aceitáveis ​​para parâmetros inteiros. Isso nos daria uma sintaxe de currying multiparâmetro muito limpa à la Open Dyln . Como _ substitui parâmetros inteiros, a sintaxe pode ser mínima, intuitiva e inequívoca. map(_ + 2, _) seria traduzido para x -> map(y -> y + 2, x) . A maioria das expressões de chamada de não função que você gostaria de lambdafy provavelmente seriam mais longas e mais amigáveis ​​para -> ou do qualquer maneira. Eu realmente acho que a compensação entre usabilidade e generalidade valeria a pena.

@durcan , isso parece promissor - você pode elaborar a regra um pouco? Por que o primeiro _ permanece dentro do argumento de map enquanto o segundo consome toda a expressão map ? Não estou certo sobre o que significa "limitar as expressões aceitáveis ​​às chamadas de função", nem o que significa "limitar as variáveis ​​/ slots aceitáveis ​​a parâmetros inteiros" ...

Ok, acho que entendi a regra, depois de ler parte da documentação do Dylan, mas tenho que me perguntar se map(_ + 2, v) funciona, mas map(2*_ + 2, v) não funciona.

Há também o negócio muito crítico de que isso significa que _ + 2 + _ significará (x,y) -> x + 2 + y enquanto _ ⊕ 2 ⊕ _ significará y -> (x -> x + 2) + y porque + e * são os únicos operadores que atualmente analisam como chamadas de função de vários argumentos em vez de como operações associativas de pares. Agora, pode-se argumentar que essa inconsistência deve ser corrigida, embora isso pareça implicar que o _parser_ tenha uma opinião sobre quais operadores são associativos e quais não são, o que parece ruim. Mas eu argumentaria que qualquer esquema que requeira saber se o analisador analisa a + b + c como uma única chamada de função ou uma chamada aninhada pode ser um tanto questionável. Talvez a notação de infixo deva ser tratada de maneira especial? Mas não, isso também parece suspeito.

Sim, você acertou em cheio. Por um lado, cadeias longas de operadores é a sintaxe que apresenta mais problemas, dadas algumas de nossas opções de análise atuais (embora seja difícil criticar a semântica de um recurso de linguagem por depender da semântica atual da linguagem). Por outro lado, longas cadeias de chamadas de função é onde ele se destaca. Por exemplo, poderíamos reescrever seu problema como:

2*_ |> 2+_ |> map(_, v)

De qualquer forma, não acho que o problema do infixo deva atrapalhar a criação de uma opção livre de delimitador limpo. Isso realmente ajudaria com a maioria das chamadas de função normais, que é o tipo de problema em questão. Se você quiser, pode ter um delimitador opcional para ajudar a resolver essa ambiguidade em particular (aqui estou roubando & para essa função):

_ ⊕ 2 ⊕ _    # y -> (x -> x + 2) + y
_ ⊕ 2 ⊕ _ &  # (y , x) -> x + 2 + y

Esta é a melhor proposta até agora, mas não estou totalmente convencida. É bastante claro o que está acontecendo quando as chamadas de função são explícitas, mas menos claro quando as chamadas de função são implícitas pela sintaxe infixa.

Gosto de pensar nessa abordagem como mais um currying flexível e generalizado, em vez de um lambda curto e doce (e mesmo assim podemos chegar lá com um delimitador opcional). Eu adoraria algo mais perfeito, mas sem adicionar mais ruído simbólico (a antítese desta questão), não tenho certeza de como chegar lá.

Sim, eu gosto, exceto pela coisa do infixo. Essa parte pode ser corrigida.

Bem, currying na posição infixa pode ser um erro de sintaxe:

map(+(*(2, _), 2), v)      # curry is OK syntax, but obviously not what you wanted
map(2*_ + 2, v)            # ERROR: syntax: infix curry requires delimitation
map(2*_ + 2 &, v)          # this means what we want
map(*(2,_) |> +(_,2), v)   # as would this

Também poderia ser um aviso, eu acho.

Chamar isso de currying me parece confuso e errado.

Claro, isso é mais como um aplicativo de função parcial (que opcionalmente se torna um lambda argumentado anonimamente), eu acho. Nomeando à parte, alguma ideia?

Estou pensando em algo assim:

  • Se _ aparecer sozinho como qualquer um dos argumentos de uma expressão de chamada de função, essa chamada de função será substituída por uma expressão de função anônima, recebendo tantos argumentos quanto a função tiver _ argumentos, cujo corpo é o expressão original com argumentos _ substituídos por argumentos lambda em ordem.
  • Se _ aparecer em outro lugar, a expressão envolvente até, mas não incluindo o nível de precedência da _ nesta expressão, cujo corpo é a expressão original com _ instâncias substituídas por argumentos lambda na ordem.

Exemplos:

  • f(_, b)x -> f(x, b)
  • f(a, _)x -> f(a, x)
  • f(_, _)(x, y) -> f(x, y)
  • 2_^2x -> 2x^2
  • 2_^_(x, y) -> 2x^y
  • map(_ + 2, v)map(x -> x + 2, v)
  • map(2_ + 2, v)map(x -> 2x + 2, v)
  • map(abs, _)x -> map(abs, x)
  • map(2_ + 2, _)x -> map(y -> 2y + 2, x)
  • map(2_ - _, v, w)map((x, y) -> 2x - y, v, w)
  • map(2_ - _, v, _)x -> map((y, z) -> 2y - z, v, x)
  • map(2_ - _, _, _)(x, y) -> map((z, w) -> 2z - w, x, y)
  • _x -> x
  • map(_, v)x -> map(x, v)
  • map((_), v)map(x -> x, v)
  • f = _f = x -> x
  • f = 2_f = x -> 2x
  • x -> x^_x -> y -> x^y
  • _ && _(x, y) -> x && y
  • !_ && _(x, y) -> !x && y

O único lugar onde isso começa a ficar perigoso são as condicionais - esses exemplos ficam meio estranhos.

Isso ainda é um pouco complicado e sem princípios e há casos esquivos, mas estou chegando a algum lugar.

Isso me parece uma péssima ideia, quase toda sintaxe em Julia é bastante familiar se você tiver usado outras linguagens de programação. Pessoas olhando para sintaxe sugar como este não terão ideia do que está acontecendo para o "benefício" de salvar alguns caracteres.

Exemplos que me deixam um pouco menos feliz:

  • 2v[_]x -> 2v[x] (bom)
  • 2f(_)2*(x -> f(x)) (não tão bom)
  • _ ? "true" : "false"(x -> x) ? "true" : "false"
  • _ ? _ : 0(x -> x) ? (y -> y) : 0

Acho que algo mais profundo está acontecendo aqui - há alguma noção de posições sintáticas nas quais um objeto de função _faz sentido_ - e você deseja expandir para o mais próximo deles. A posição clássica que "deseja" um objeto de função é um argumento para outra função, mas o lado direito de uma atribuição é outro lugar que pode ser dito que "deseja" um objeto de função (ou talvez seja mais preciso dizer que prefere assumir uma função como sendo o corpo de uma função).

Talvez, mas o mesmo argumento poderia ser (e foi) feito sobre a sintaxe do-block, que eu acho, no geral, tem sido muito útil e bem-sucedida. Isso está intimamente relacionado a uma melhor sintaxe para vetorização. Também não é tão inédito - Scala usa _ forma semelhante, e Mathematica usa # forma semelhante.

Eu acho que você também poderia argumentar que a escolha _unfamiliar_ de
despacho múltiplo em vez de um operador ponto de despacho único essencialmente
obriga a decisão de ter sintaxe sucinta para argumentos pronominais para
recuperar a ordem SVO com a qual as pessoas estão familiarizadas.

Em Ter, 17 de novembro de 2015 às 12h09 Stefan Karpinski [email protected]
escrevi:

Talvez, mas o mesmo argumento poderia (e foi) feito sobre o do-block
sintaxe, que penso, no geral, tem sido muito útil e bem-sucedida.
Isso está intimamente relacionado a uma melhor sintaxe para vetorização. Também não é
que sem precedentes - Scala usa _ de forma semelhante, e Mathematica usa #
similarmente.

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

Isso também existe em C ++ com várias soluções de biblioteca em Boost em particular, que usam _1, _2, _3 como argumentos (por exemplo, _1(x, y...) = x , _2(x, y, z...) = y etc), sendo a limitação para ser capaz de chamada, por exemplo, fun(_1) para x -> fun(x) , fun deve ser explicitamente compatível com a biblioteca (normalmente através de uma chamada de macro, para fazer fun aceitar um "tipo lambda "como parâmetro).

Eu realmente gostaria que esta notação lambda concisa estivesse disponível em Julia.
Em relação ao problema de 2f(_) desugaring para 2*(x -> f(x)) : faria sentido modificar as regras ao longo das linhas de "se a primeira regra se aplica, por exemplo f(_) , então re avaliar recursivamente as regras com f(_) desempenhando o papel de _ . Isso permitiria também, por exemplo, f(g(_))x -> f(g(x)) , com a "regra dos parênteses" permitindo facilmente pare no nível desejado, por exemplo, f((g(_)))f(x->g(x)) .

Eu gosto muito do nome "notação lambda concisa" para isso. (Muito melhor do que currying).

Eu realmente preferiria a explicitação de _1 , _2 , _3 se você passar lambdas de vários argumentos. Em geral, eu geralmente acho que reutilizar nomes de variáveis ​​no mesmo escopo pode ser confuso ... e ter _ sendo xey na _a mesma expressão_ parece muito confuso.

Descobri que a mesma sintaxe concisa _ -sintaxe causou um pouco de confusão (veja todos os usos de _ em scala ).

Além disso, muitas vezes você deseja fazer:

x -> f(x) + g(x)

ou semelhante, e acho que ficaria surpreso se o seguinte não funcionasse:

f(_) + g(_)

Além disso, você pode querer mudar a ordem dos argumentos:

x, y -> f(y, x)
f(_2, _1)  # can't do with other suggested _ syntax

Eu acho que seria bom para a sintaxe permitir a numeração de argumento anônimo explícito ( _1 , _2 , _3 ... etc.), Mas o problema principal ainda permanece : quando exatamente você promove uma função parcialmente aplicada a um lambda conciso? E o que exatamente é o corpo lambda? Eu provavelmente erraria por ser explícito (com um delimitador) em vez de usar implicitamente algum tipo de regras de promoção complexas. O que deveria

foo(_1, _1 + _2  + f(_1, v1) + g(_2, v3), _3 * _2, v2) + g(_4, v4) +
 f(_2, v2) + g(_3, v5) + bar(_1, v6)

significa exatamente? Usando um delimitador (usarei λ ), as coisas são um pouco mais claras:

λ(foo(_1, λ(_1 + _2)  + λ(f(_1, v1) + g(_2, v3)), _3 * _2, v2) + g(_4, v4)) + 
λ(f(_2, v2) + g(_3, v5) + bar(_1, v6))

Obviamente, isso é MethodError: + has no method matching +(::Function, ::Function) , mas pelo menos posso dizer isso pela maneira como está escrito.

Eu realmente acho que @StefanKarpinski pode estar

Esta é definitivamente uma compensação entre concisão versus generalidade e legibilidade. É claro que não faz sentido introduzir algo menos conciso do que a notação -> . Mas também acho que um delimitador vale a pena.

Talvez não seja sucinto o suficiente, mas que tal uma versão de prefixo de -> que captura _ argumentos? Por exemplo

(-> 2f(_) + 1)

Eu acho que a forma de prefixo deve ter precedência muito baixa. Isso pode permitir, na verdade, deixar de fora os parênteses em alguns casos, por exemplo

map(->_ + 1, x)

No momento, estou mexendo na implementação de https://github.com/JuliaLang/julia/issues/5571#issuecomment -157424665

Como uma macro, isso transforma todas essas ocorrências na linha.
A parte complicada é implementar precedência.

Provavelmente não vou terminar nas próximas 12 horas porque é hora de voltar para casa aqui
(talvez nos próximos 24, mas talvez eu tenha que ir embora)
De qualquer forma, uma vez feito isso, podemos brincar com isso.

Um estranho que sai de https://github.com/JuliaLang/julia/issues/5571#issuecomment -157424665

  • f(_,_)x,y -> f(x,y) (isso é razoável)
  • f(_,2_) → ??

    • f(_,2_)x,y -> f(x,2y) (razoável)

    • f(_,2_)x-> f(x,y->2y) (o que eu acho que a regra está sugerindo e o que meu protótipo produz)

Mas não tenho certeza se entendi direito.

Então aqui está meu protótipo.
http://nbviewer.ipython.org/gist/oxinabox/50a1e17cfb232a7d1908

Na verdade, ele definitivamente falha em alguns dos testes.

Não é possível considerar o bracketing na camada AST atual - eles geralmente (sempre?) Já estão resolvidos.

Ainda assim, é o suficiente para brincar, eu acho

Algumas regras de magrittr em R que podem ser úteis:

Se uma cadeia começa com. , é uma função anônima:

. %>% `+`(1)

é o mesmo que função (x) x + 1

Existem dois modos de encadeamento:
1) Encadeamento inserindo como primeiro argumento, bem como quaisquer pontos que apareçam.
2) Encadeamento apenas aos pontos que aparecem.

O modo padrão é o modo 1. No entanto, se o ponto aparecer sozinho como um argumento para a função que está sendo encadeada, então magrittr muda para o modo 2 para essa etapa da cadeia.

então

2 %>% `-`(1) 

é 2 - 1,

e

1 %>% `-`(2, . )

também é 2 - 1

O Modo 2 também pode ser especificado entre colchetes:

2 %>% { `-`(2, . - 1) }

seria o mesmo que 2 - (2 - 1).

Além disso, apenas uma observação de que ser capaz de alternar inteligentemente entre o modo 1 e o modo 2 resolve quase completamente o problema de Julia não ser muito consistente em ter o argumento que provavelmente ficaria encadeado na primeira posição. Eu também esqueci que observar que os colchetes podem permitir a avaliação de um pedaço de código. Aqui está um exemplo do manual magrittr:

iris%>%
{
n <- amostra (1:10, tamanho = 1)
H <- cabeça (., N)
T <- cauda (., N)
rbind (H, T)
}%>%
resumo

Esta é apenas uma ideia parcialmente formada no momento, mas eu me pergunto se há uma maneira de resolvermos os problemas de "lambda conciso" e "variável fictícia" ao mesmo tempo, modificando o construtor Tuple de forma que um valor ausente retorne um lambda que retorna uma tupla em vez de uma tupla? Portanto, (_, 'b', _, 4) retornaria (x, y) -> (x, 'b', y, 4) .

Então, se alterarmos sutilmente a semântica da chamada de função de forma que foo(a, b) signifique "aplicar foo à tupla (a, b) ou se o argumento for uma função, aplique foo à tupla retornada pela função ". Isso tornaria foo(_, b, c)(1) equivalente a apply(foo, ((x) -> (x, b, c))(1)) .

Acho que isso ainda não resolve o problema da notação do infixo, mas pessoalmente ficaria feliz com lambdas concisas que só funcionam com chamadas de função entre parênteses. Afinal, 1 + _ sempre pode ser reescrito +(1, _) se for absolutamente necessário.

@jballanc No entanto, a construção de tupla e a aplicação de funções são dois conceitos bem distintos. Pelo menos, a menos que minha compreensão da semântica de Julia seja seriamente falha.

@samuela O que eu quis dizer com isso é que foo(a, b) é equivalente a foo((a, b)...) . Ou seja, os argumentos para uma função podem ser conceitualmente pensados ​​como uma Tupla, mesmo que a Tupla nunca seja construída na prática.

@ Tentei ler esta discussão, mas é muito longo para eu manter o controle de tudo o que foi dito - desculpe se estou repetindo mais do que o necessário aqui.

Eu gostaria apenas de votar por fazer de |> um complemento para a do "mágica". Pelo que posso ver, a maneira mais fácil de fazer isso é deixar que signifique

3 |> foo == foo(3) # or foo() instead of just foo, but it would be nice if the parentheses were optional
3 |> foo(1) == foo(1, 3)
3 |> foo(1,2) == foo(1,2,3)

Em outras palavras, a |> f(x) faz com o _last_ argumento o que f(x) do; a; end faz com _primeiro_. Isso o tornaria imediatamente compatível com map , filter , all , any et. al., sem adicionar a complexidade de definir o escopo dos parâmetros _ , e dada a sintaxe do já existente, não acho que isso coloque uma carga conceitual excessiva sobre os leitores do código.

Minha principal motivação para usar um operador de tubo como este são os pipelines de coleta (ver # 15612), que eu acho que é uma construção incrivelmente poderosa e que está ganhando terreno em muitas línguas (indicando que é um recurso que as pessoas desejam e um eles vão entender).

Essa é a macro @>> de https://github.com/MikeInnes/Lazy.jl.

@malmaud Nice! Gosto que isso já seja possível: D

No entanto, a diferença de legibilidade entre essas duas variantes é muito grande:

# from Lazy.jl
@> x g f(y, z)

# if this became a first-class feature of |>
x |> g |> f(y, z)

Acho que o principal problema de legibilidade é que não há pistas visuais para dizer onde estão os limites entre as expressões - os espaços em x g e g f(x, afetarão significativamente o comportamento do código, mas o espaço em f(x, y) não.

Como @>> já existe, quão viável é adicionar esse comportamento a |> em 0,5?

(Não quero gastar muita energia na notação do operador, então não descarte esta proposta apenas na questão da notação. No entanto, podemos notar que "tubulação" parece uma noção natural para isso (cf o termo "pipeline de coleta"), e que, por exemplo, F # já usa |> para isso, embora, é claro, tenha uma semântica ligeiramente diferente, já que as funções F # são diferentes das funções Julia.)

Sim, com certeza concordo com a frente de legibilidade. Não seria tecnicamente desafiador fazer |> se comportar como você está descrevendo em 0.5, é apenas uma questão de design.

Da mesma forma, seria possível fazer as funções de análise de macro de Lazy.jl @>> encadeadas por |> .

Hm. Vou começar a trabalhar em um PR para Lazy.jl então, mas isso não significa que eu gostaria que isso não estivesse em 0.5 :) Acho que não sei o suficiente sobre o analisador Julia e como mudar o comportamento de |> para ajudar com isso, no entanto, a menos que eu obtenha uma orientação bastante extensa.

Acho que não mencionei neste tópico, mas tenho outro pacote de encadeamento, ChainMap.jl. Ele sempre substitui em _ e é inserido condicionalmente no primeiro argumento. Ele também tenta integrar o mapeamento. Veja https://github.com/bramtayl/ChainMap.jl

Nossa lista atual de vários esforços, etc.
Acho que vale a pena que as pessoas verifiquem isso (de preferência antes de opinar, mas w / e)
eles são ligeiramente diferentes.
(Estou tentando fazer o pedido cronologicamente).

Pacotes

Protótipos de não-embalagem

Relacionado:


Talvez isso deva ser editado em uma das principais postagens.

atualizado: 20-04-2020

Este é mais um tipo de experimento de sistema do que uma tentativa real de implementar um aplicativo parcial, mas aqui está um estranho: https://gist.github.com/fcard/b48513108a32c13a49a387a3c530f7de

uso:

include("partial_underscore_generated.jl")
using GeneratedPartial

const sub = partialize(-)
sub(_,2)(1) == 1-2
sub(_,_)(1,2) == 1-2
sub(_,__)(1)(2) == 1-2
sub(__,_)(2)(1) == 1-2 #hehehe

# or
<strong i="8">@partialize</strong> 2 Base.:+ # evily inserts methods in + and allows partializations for 2 arguments
(_+2)(1) == 1+2

# fun:
sub(1+_,_)(2,3) == sub(1+2,3)
sub(1+_,__)(2)(3) == sub(1+2,3)
(_(1)+_)(-,1) == -1+1

# lotsafun:
appf(x::Int,y::Int) = x*y
appf(f,x) = f(x)
<strong i="9">@partialize</strong> 2 appf

appf(1+_,3)(2) == appf(1+2,3)
appf(?(1+_),3) == appf(x->(1+x), 3)
appf(?sub(_,2),3) == appf(x->x-2,3) # I made a method *(::typeof(?),::PartialCall), what of it!!?

# wooooooooooooooooooooooooooooooooo
const f = sub
f(_,f(_,f(_,f(_,f(_,f(_,f(_,f(_,f(_,_)))))))))(1,2,3,4,5,6,7,8,9,10) == f(1,f(2,f(3,f(4,f(5,f(6,f(7,f(8,f(9,10)))))))))
f(_,f(__,f(___,f(____,f(_____,f(______,f(_______,f(________,f(_________,__________)))))))))(1)(2)(3)(4)(5)(6)(7)(8)(9)(10) == f(1,f(2,f(3,f(4,f(5,f(6,f(7,f(8,f(9,10)))))))))

# this answers Stefan's concern (which inspired me to make this hack in the first place)
#
#    const pmap = partialize(map)
#    map(f(_,a),   v) == map(x->f(x,a), v)
#    pmap(?f(_,a), v) == map(x->f(x,a), v)
#    pmap(f(_,a),  v) == x->map(f(x,a), v)
#
# it adds a few other issues, of course...


Certamente não é uma proposta de implementação, mas acho divertido brincar com ela, e talvez alguém consiga tirar meia boa ideia dela.

PS Esqueci de mencionar que @partialize também funciona com intervalos e literais de matriz de inteiros:

<strong i="15">@partialize</strong> 2:3 Base.:- # partialized for 2 and 3 arguments!

(_-_-_)(1,2,3) == -4
(_-_+_)(1,2,3) == +2

OK, estive pensando sobre a composição de funções e, embora IMO esteja claro no caso SISO, acho que estava pensando em como uso o MISO de Julia (quase MIMO?) Em pequenos blocos de código encadeados.

Eu gosto do REPL. Muito. É fofo, é legal, permite que você experimente como o MATLAB, o python ou o shell. Gosto de pegar o código de um pacote ou arquivo e copiá-lo e colá-lo no REPL, até mesmo várias linhas de código. O REPL avalia isso em cada linha e me mostra o que está acontecendo.

Ele também retorna / define ans após cada expressão. Todo usuário do MATLAB sabe disso (embora, neste estágio, este seja um argumento ruim!). Provavelmente, a maioria dos usuários do Julia já o viu / usou antes. Eu uso ans nas situações estranhas em que estou brincando com algo por peça, percebendo que quero adicionar mais uma etapa ao que escrevi acima. Eu não gosto que usá-lo seja meio destrutivo, então eu tendo a evitá-lo quando possível, mas _toda_ a proposta aqui é lidar com vidas de retorno de apenas uma etapa de composição.

Para mim, _ ser mágico é apenas _odd_, mas entendo que muitas pessoas podem discordar. Portanto, se eu _quero_ copiar e colar o código de pacotes no REPL e assisti-lo em execução, e se eu quiser uma sintaxe que não parece mágica, então posso propor:

<strong i="12">@repl_compose</strong> begin
   sin(x)
   ans + 1
   sqrt(ans)
end

Se a função retornar várias saídas, posso inserir ans[1] , ans[2] , etc. na próxima linha. Ele já se encaixa perfeitamente no modelo de composição de nível único, o modelo MISO de Julia, e já _é uma sintaxe muito padrão de Julia_, mas não em arquivos.

A macro é fácil de implementar - basta converter Expr(:block, exprs...) em Expr(:block, map(expr -> :(ans = $expr), exprs) (também um let ans no início, e talvez possa haver uma versão que faça uma função anônima que leva um entrada ou algo assim?). Não _teria_ viver na base (embora o REPL esteja embutido em Julia, e meio que vai com isso).

Enfim, apenas minha perspectiva! Esse foi um longo fio, que eu não olhava há muito tempo!

Ele também retorna / define ans após cada expressão. Todo usuário do MATLAB sabe disso (embora, neste estágio, este seja um argumento ruim!).

Na verdade, o outro argumento é que se _ for usado no encadeamento de funções, o REPL também deve retornar _ vez de ans (para mim, isso seria o suficiente para remover a mágica").

Existem muitos precedentes para usar _ como o "valor de it" nas línguas. Claro, isso entra em conflito com a ideia proposta de usar _ como um nome que descarta atribuições e para lambdas terser.

Tenho certeza que isso existe em algum lugar em Lazy.jl como <strong i="5">@as</strong> and begin ...

A ideia de usar . para encadeamento foi descartada no início da conversa, mas tem um longo histórico de utilidade e minimizaria a curva de aprendizado para usuários de outras linguagens. A razão pela qual é importante para mim é porque

type Track
  hit::Array{Hit}
end
type Event
  track::Array{Track}
end

event.track[12].hit[43]

me dá o 43º hit da 12ª faixa de um evento quando track e hit são matrizes simples, então

event.getTrack(12).getHit(43)

deve me dar a mesma coisa se eles tiverem que ser servidos dinamicamente. Eu não quero ter que dizer

getHit(getTrack(event, 12), 43)

Fica pior quanto mais fundo você vai. Como essas funções são simples, o argumento é mais amplo do que o do encadeamento de funções (a la Spark).

Estou escrevendo isso agora porque acabei de aprender sobre os traços de Rust , o que poderia ser uma boa solução em Julia pelos mesmos motivos. Como Julia, Rust tem apenas dados structs (Julia type ), mas também tem impl para funções de ligação ao nome de struct . Pelo que eu posso dizer, é açúcar sintático puro, mas permite a notação de ponto que descrevi acima:

impl Event {
  fn getTrack(&self, num: i32) -> Track {
    self.track[num]
  }
}

impl Track {
  fn getHit(&self, num: i32) -> Track {
    self.track[num]
  }
}

que em Julia poderia ser

impl Event
  function getTrack(self::Event, num::Int)
    self.track[num]
  end
end

impl Track
  function getHit(self::Track, num::Int)
    self.hit[num]
  end
end

A sintaxe proposta acima não faz nenhuma interpretação de self : é apenas um argumento de função, portanto, não deve haver conflitos com despacho múltiplo. Se você quiser fazer uma interpretação mínima de self , você pode tornar o tipo do primeiro argumento implícito, de modo que o usuário não precise digitar ::Event e ::Track em cada função, mas uma coisa boa sobre não fazer qualquer interpretação é que "métodos estáticos" são apenas funções em impl que não têm self . (Rust os usa para fábricas de new .)

Ao contrário de Rust, Julia tem uma hierarquia em types . Ele também pode ter uma hierarquia semelhante em impls para evitar a duplicação de código. Um OOP padrão poderia ser construído tornando as hierarquias de dados type e método impl exatamente iguais, mas esse espelhamento estrito não é necessário e, em alguns casos, indesejável.

Há um ponto fixo nisso: suponha que eu nomeie minhas funções track e hit em impl , em vez de getTrack e getHit , de modo que eles entraram em conflito com os arrays track e hit em type . Então event.track retornaria o array ou a função? Se você usá-lo imediatamente como uma função, isso pode ajudar a desambiguar, mas types pode conter Function objetos também. Talvez apenas aplique uma regra geral: após o ponto, verifique primeiro o impl , depois verifique o type ?

Pensando bem, para evitar ter dois "pacotes" para o que é conceitualmente o mesmo objeto ( type e impl ), que tal:

function Event.getTrack(self, num::Int)
  self.track[num]
end

para ligar a função getTrack a instâncias do tipo Event modo que

myEvent.getTrack(12)

produz o mesmo bytecode que a função aplicada a (myEvent, 12) ?

O que há de novo é a sintaxe typename-ponto-functionname após a palavra-chave function e como ela é interpretada. Isso ainda permitiria o despacho múltiplo, um semelhante ao Python self se o primeiro argumento for o mesmo que o tipo ao qual está vinculado (ou deixado implícito, como acima), e permite um "método estático" se o primeiro argumento não está presente ou foi digitado de maneira diferente do tipo ao qual está vinculado.

@jpivarski Existe uma razão para você achar que a sintaxe de pontos (que, lendo este tópico, tem muitas desvantagens) é melhor do que alguma outra construção que permite o encadeamento? Ainda acho que criar algo como do mas para o último argumento , apoiado por alguma forma de sintaxe de piping (por exemplo, |> ) seria o melhor caminho a seguir:

event |> getTrack(12) |> getHit(43)

A principal razão pela qual posso ver que algo como a abordagem de Rust poderia ser melhor é que ele usa efetivamente o lado esquerdo como um namespace para funções, então você pode ser capaz de fazer coisas como parser.parse sem entrar em conflito com o existente Função Julia Base.parse . Eu seria a favor de fornecer a proposta Rust e a tubulação do estilo Hack.

@tlycken Essa é uma sintaxe ambígua, dependendo da precedência.
Lembrar da Precidência de |> vs call pode ser confuso, uma vez que não dá realmente nenhuma dica.
(Nem várias das outras opções sugeridas.)

Considerar

foo(a,b) = a+b
foo(a) = b -> a-b

2 |> foo(10) == 12   #Pipe Precedence > Call Precedence 
2 |> foo(10) == 8     #Pipe Precedence < Call Precedence   

@oxinabox Na verdade, não estou sugerindo que seja "apenas" um operador regular, mas sim um elemento de sintaxe da linguagem; 2 |> foo(10) desugars para foo(10, 2) da mesma forma que foo(10) do x; bar(x); end desugars para foo(x -> bar(x), 10) . Isso implica precedência de pipe sobre precedência de chamada (o que, eu acho, é o que faz mais sentido de qualquer maneira).

Apenas no que diz respeito à sintaxe, . é menos obstrutivo visualmente e certamente mais padrão do que |> . Posso escrever uma cadeia fluente de funções separadas por . sem espaços (um caractere cada) e qualquer pessoa pode lê-la; com |> , eu teria que adicionar espaços (quatro caracteres cada) e seria um speedbump visual para a maioria dos programadores. A analogia com | do script de shell é boa, mas não imediatamente reconhecível por causa do > .

Estou lendo este tópico corretamente que o argumento contra o ponto é que é ambíguo se ele deve obter um dado de membro de type ou uma função de membro de impl (minha primeira sugestão) ou a função namespace (minha segunda sugestão)? Na segunda sugestão, as funções definidas no namespace da função criado por um type devem ser definidas _após_ a definição de type , para que possam se recusar a ofuscar um dado membro ali.

Adicionar os dois pontos de namespace (minha segunda sugestão) e |> seria ótimo para mim; eles são bastante diferentes em propósito e efeito, apesar do fato de que ambos podem ser usados ​​para encadeamento fluente. No entanto, |> conforme descrito acima não é completamente simétrico com do , uma vez que do requer que o argumento que ele preenche seja uma função. Se você está dizendo event |> getTrack(12) |> getHit(43) , então |> aplica-se a não funções ( Events e Tracks ).

Se você está dizendo event |> getTrack(12) |> getHit(43) , então |> aplica-se a não funções ( Events e Tracks ).

Na verdade, não - ele se aplica aos encantamentos de função _ à sua direita_ inserindo seu operando à esquerda como o último argumento para a chamada de função. event |> getTrack(12) é getTrack(12, event) por causa do que estava à direita, não por causa do que estava à esquerda.

Isso significaria a) precedência sobre chamadas de função (já que é uma reescrita da chamada) eb) ordem de aplicação da esquerda para a direita (para torná-la getHit(43, getTrack(12, event)) vez de getHit(43, getTrack(12), event) ) .

Mas a assinatura getTrack's é

function getTrack(num::Int, event::Event)

portanto, se event |> getTrack(12) inserir event em getTrack's último argumento, estará colocando Event no segundo argumento, não Function . Acabei de tentar o equivalente com do e o primeiro argumento, e Julia 0.4 reclamou que o argumento precisa ser uma função. (Possivelmente porque do event end é interpretado como uma função que retorna event , em vez do próprio event .)

O encadeamento de funções parece, para mim, ser um problema separado de muito do que está sendo discutido em torno da sintaxe do ponto ( . ). Por exemplo, @jpivarski , você já pode realizar muito do que mencionou em Rust in Julia sem quaisquer novos recursos:

type TownCrier
  name::AbstractString
  shout::Function

  function TownCrier(name::AbstractString)
    self = new(name)
    self.shout = () -> "HELLO, $(self.name)!"
    self
  end
end

tc = TownCrier("Josh")
tc.shout()                                #=> "HELLO, Josh!"
tc.name = "Bob"
tc.shout()                                #=> "HELLO, Bob!"

Sem tentar atrapalhar muito a conversa, sugiro que o que realmente precisamos resolver é como realizar uma função eficiente de currying em Julia. As perguntas sobre como especificar posições para argumentos em uma cadeia de funções desapareceriam se tivéssemos uma boa maneira de curry funções. Além disso, construções como as acima seriam mais limpas se o corpo da função pudesse ser especificado e simplesmente curvado com "self" na construção.

@andyferris Tenho usado Python e gosto muito de _ referir-me ao resultado da expressão anterior. Porém, não funciona dentro de funções. Seria ótimo se pudéssemos fazê-lo funcionar em qualquer lugar: dentro de blocos iniciais, funções, etc.

Acho que isso pode substituir totalmente o encadeamento. Não deixa nenhuma ambiguidade sobre a ordem do argumento. Por exemplo,

begin
    1
    vcat(_, 2)
    vcat(3, _)
end

# [3, 1, 2]

Como @MikeInnes mencionou, isso já está disponível em @_ em Lazy.jl (e embora não tenha funcionado dessa forma originalmente, ChainMap.jl também usa esse tipo de encadeamento agora).

Esperançosamente, isso poderia funcionar junto com a fusão de pontos, pelo menos dentro de blocos

begin
    [1, 2, 3]
    .+(_, 2)
    .*(_, 2)
    .-(10, _)
end

ou, usando a sintaxe @chain_map ,

begin
    ~[1, 2, 3]
    +(_, 2)
    *(_, 2)
    -(10, _)
end

Atualmente existe uma maneira de encadear funções com objetos se a função for definida dentro do construtor. Por exemplo, a função Obj.times:

type Obj
    x
    times::Function
    function Obj(x)
       this = new(x)
       this.times =  (n) -> (this.x *= n; this)
       this
    end
end

>>>Obj(2).times(3)
Obj(6,#3)

E sobre a implementação de funções de membro (funções especiais) definidas fora da definição de tipo. Por exemplo, a função Obj.times seria escrita como:

member function times(this::Obj, n)
     this.x *= n
     return this
end

>>>Obj(2).times(3)
Obj(6,#3)

onde member é uma palavra-chave especial para funções-membro.
As funções de membro têm acesso aos dados do objeto. Posteriormente, eles serão chamados usando um ponto após a variável do objeto.
A ideia é reproduzir o comportamento de funções definidas dentro de construtores usando funções de "membro" definidas fora da definição de tipo.

Algo assim é feito em Rust com sintaxe de método . É conceitualmente distinto do encadeamento de funções, embora possa ser usado para fazer o encadeamento se parecer com o de algumas linguagens OO. Provavelmente, é melhor abordar os problemas separadamente.

Eu li isso e alguns assuntos relacionados, aqui está minha proposta:

Encadeamento básico :
in1 |> function1
O mesmo que: in1 |> function1(|>)

in2 |> function2(10)
O mesmo que: in2 |> function2(|>,10)

Ainda mais encadeamento:
in1 |> function1 |> function2(10,|>)

Ramificação e fusão da cadeia:
Ramifique duas vezes com ramificações out1 , out2 :
function1(a) |out1>
function2(a,b) |out2>

Use o branch out1 e out2 :
function3(|out1>,|out2>)

E a preguiça?
Precisamos de algo como a convenção function!(mutating_var) ?
Para funções preguiçosas, poderíamos usar function?() ...

E, usando o recuo de maneira adequada, é fácil rastrear visualmente as dependências de dados no gráfico de chamadas associado.

Eu apenas brinquei com um padrão para encadeamento de funções com o operador |> . Por exemplo, estas definições:
`` `` julia

Filtro

MyFilter imutável {F}
flt :: F
fim

function (mf :: MyFilter) (fonte)
filtro (mf.flt, fonte)
fim

função Base.filter (flt)
MyFilter (flt)
fim

Levar

imutável MyTake
n :: Int64
fim

function (mt :: MyTake) (fonte)
pegue (fonte, mt.n)
fim

função Base.take (n)
MyTake (n)
fim

Mapa

imutável MyMap {F}
f :: F
fim

função (mm :: MyMap) (fonte)
mapa (mm.f, fonte)
fim

função Base.map (f)
MyMap (f)
fim
enable this to work: julia
1:10 |> filtro (i-> i% 2 == 0) |> pegar (2) |> mapa (i-> i ^ 2) |> coletar
`` Essentially the idea is that functions like filter return a functor if they are called without a source argument, and then these functors all take one argument, namely whatever is "coming" from the left side of the |> . The |> `` então apenas encadeia todos esses functores juntos.

filter etc. também poderia simplesmente retornar uma função anônima que leva um argumento, não tendo certeza de qual dessas opções teria melhor desempenho.

No meu exemplo, estou substituindo map(f::Any) no Base, não entendo realmente o que a definição existente de map faz ...

Acabei de inventar esse padrão, e meu olhar um tanto superficial ao redor não mostrou nenhuma discussão sobre algo assim. O que as pessoas pensam? Isso pode ser útil? As pessoas podem pensar nas desvantagens disso? Se isso funcionar, talvez o design existente seja realmente flexível o suficiente para permitir uma história de encadeamento bastante abrangente?

Isso não parece viável para funções arbitrárias, apenas aquelas para as quais MyF foi definido?

Sim, isso funciona apenas para funções ativadas. Claramente não é uma solução muito geral e, em certo sentido, a mesma história da vetorização, mas ainda, dado que nem tudo que esperamos chegará a 1.0, esse padrão pode permitir uma série de cenários onde as pessoas tiveram que recorrer a macros agora.

Essencialmente, a ideia é que funções como filtro retornam um functor se forem chamadas sem um argumento de origem, e então todos esses functores recebem um argumento, a saber, tudo o que está "vindo" do lado esquerdo do |>.

Esta é, quase exatamente, a essência dos transdutores de Clojure. A diferença notável é que a Clojure construiu transdutores com base no conceito de redutores. Resumindo, toda função que opera em uma coleção pode ser decomposta em uma função de "mapeamento" e uma função de "redução" (mesmo se a função de "redução" for simplesmente concat ). A vantagem de representar funções de coleção dessa maneira é que você pode reorganizar a execução de modo que todo o "mapeamento" possa ser canalizado (especialmente para grandes coleções). Os transdutores, então, são apenas uma extração desses "mapeamentos" retornados quando chamados sem uma coleção para operar.

Não há necessidade de que isso seja tão complicado. As funções podem optar por currying com encerramentos:

Base.map(f)    = (xs...) -> map(f, xs...)
Base.filter(f) = x -> filter(f, x)
Base.take(n)   = x -> take(x, n)

Obviamente, isso não é algo que um pacote deva fazer, já que muda o significado desses métodos para todos os pacotes. E fazer isso aos poucos, assim, não é terrivelmente intuitivo - quais argumentos devem ter prioridade?

Eu prefiro uma solução sintática de call-site como foi discutido acima, reduzindo f(a, _, b) para x -> f(a, x, b) . É complicado, porém, conforme observado na longa discussão acima.

Não há necessidade de que isso seja tão complicado. As funções podem optar por currying com fechamentos

Sim, já sugeri isso acima, só não tinha certeza se há uma diferença de desempenho entre esses dois.

quais argumentos devem ter prioridade?

Sim, e então temos coisas como filter e take , onde em um caso temos a coleção como o primeiro e no outro como o último argumento ... Eu meio que sinto que pelo menos para operações do tipo iterador, pode haver uma resposta óbvia para essa pergunta.

Uma vez que _ é um símbolo especial disponível

Sim, concordo totalmente que existe uma solução mais geral e pode ser a de @malmaud .

Não há diferença de desempenho, pois os fechamentos basicamente apenas geram o código que você escreveu à mão de qualquer maneira. Mas já que você está apenas currying, você poderia escrever uma função para fazer isso por você ( curry(f, as...) = (bs...) -> f(as..., bs...) ). Isso cuida do mapa e do filtro; também houve propostas no passado para implementar um curry que implementa um valor sentinela como curry(take, _, 2) .

Eu vim aqui, porque atualmente estou aprendendo três idiomas: D, Elixir e agora Julia. Em D existe a sintaxe de chamada de função uniforme , como em Rust, em Elixir você tem o operador de pipe . Ambos implementam basicamente o tipo de encadeamento de funções sugerido aqui e eu realmente gostei desse recurso em ambas as linguagens, já que é fácil de entender, parece fácil de implementar e pode tornar o código usando streams e outros tipos de pipelines de dados muito mais legíveis.

Eu só vi um breve tutorial sobre a sintaxe de julia até agora, mas imediatamente pesquisei esse recurso no Google, porque esperava que Julia também tivesse algo assim. Então eu acho que este é um +1 para esta solicitação de recurso da minha parte.

Oi pessoal,

Permita-me marcar esta solicitação de recurso com +1. Isso é extremamente necessário. Considere a seguinte sintaxe Scala.

Array(1,2,3,4,5)
  .map(x => x+1)
  .filter(x => x > 5)
  .reduce(_ + _)

Pessoas que usaram Spark ou outras ferramentas de big data baseadas em MapReduce estarão muito familiarizadas com essa sintaxe e terão escrito trabalhos grandes e complicados dessa maneira. Mesmo R, relativamente antigo, permite o seguinte.

c(1,2,3,4,5) %>%
  {. + 1} %>%
  {.[which(. > 5)]} %>%
  sum

Observe o uso inteligente de blocos de código como um substituto para a programação funcional adequada - não os mais bonitos, mas poderosos. Na Julia, posso fazer o seguinte.

[1,2,3,4,5] |> 
  _ -> map(__ -> __ + 1, _) |>
  _ -> filter(__ -> __ < 5, _) |>
  _ -> reduce(+, _)

Mas isso é horrível e feio. Se não podemos ter código orientado a objetos à la Scala, os operadores de pipe tornam-se incrivelmente importantes. A solução mais simples é o tubo alimentar o _primeiro argumento_, a menos que um caractere curinga como _ seja usado explicitamente, mas isso só faria sentido se map fosse alterado para tomar a estrutura de dados como primeiro argumento e função como segundo.

Também deve haver algum equivalente de Array(1,2,3).map(_ + 1) do Scala para evitar _ -> _ + 1 excessivos e sintaxe semelhante. Eu gosto da ideia acima, onde [1,2,3] |> map(~ + 1, _) é traduzido para map(~ -> ~ + 1, [1,2,3]) . Obrigado por olhar.

Para o último, temos transmissão com sintaxe compacta [1, 2, 3] .+ 1 É bastante viciante. Algo assim para redução (e talvez filtragem) seria incrivelmente legal, mas parece uma grande pergunta.

É um ponto razoável observar que tanto piping quanto do lutam pelo primeiro argumento da função.

Vou relembrar que o novo tópico vem, que temos
não um, não dois, mas CINCO pacotes que fornecem extensões para a funcionalidade de tubulação SISO de base do julia, para as sintaxes sugeridas.
veja a lista em: https://github.com/JuliaLang/julia/issues/5571#issuecomment -205754539

É um ponto razoável observar que tanto piping quanto lutam pelo primeiro argumento da função.

Se obtivéssemos uma funcionalidade extra de tubulação na base, essa posição não estava marcada com _ etc.
Então eu pensaria que acrescentaria argumentos à posição final, não à primeira.
isso o tornaria mais como "fingir currying / aplicação parcial"

Minha postagem acima pretende ser um exemplo simples projetado para ilustrar os problemas de sintaxe em questão.

Na realidade, muitas vezes centenas de operações são usadas em uma cadeia, muitas delas fora do padrão. Imagine trabalhar com big data de linguagem natural. Você escreve uma sequência de transformações que pega uma string de caracteres, filtra caracteres chineses, divide por espaços em branco, filtra palavras como "o", transforma cada palavra em uma forma "padronizada" por meio de uma ferramenta de software de caixa preta usada por sua web servidor, acrescente informações sobre o quão comum é cada palavra por meio de outra ferramenta de caixa preta, some os pesos de cada palavra única, 100 outras operações, etc, etc.

Estas são as situações que estou considerando. Fazer tais operações sem usar chamadas de método ou canais não é um iniciante devido ao tamanho absoluto.

Não sei qual é o melhor design, simplesmente encorajo todos a considerarem os casos de uso e uma sintaxe mais elaborada do que a que está atualmente em vigor.

Isso deve funcionar em Juno e Julia 0,6

`` `{julia}
usando LazyCall
@lazy_call_module_methods Gerador de base
Filtro de iterador

usando ChainRecursive
start_chaining ()


```{julia}
[1, 2, 3, 4, 5]
<strong i="14">@unweave</strong> ~it + 1
Base.Generator(it)
<strong i="15">@unweave</strong> ~it < 5
Iterators.filter(it)
reduce(+, it)

Tenho uma pergunta sobre alguma sintaxe que vi nos comentários sobre este assunto:
https://stackoverflow.com/questions/44520097/method-chaining-in-julia

@somedadaism , os problemas são para os problemas e não para "anunciar" perguntas que estão sobrecarregadas. Além disso, Julia-people são muito ativos no SO e (ainda mais) em https://discourse.julialang.org/. Eu ficaria muito surpreso se você não obtivesse uma resposta para a maioria das perguntas lá muito rapidamente. E, bem-vinda a Julia!

Caramba, inacreditável como isso pode ser complicado. 1 para alguma sintaxe decente. Para mim, o uso principal da tubulação também é trabalhar com dados (frames). Pense em dplyr. Pessoalmente, eu realmente não me importo em passar primeiro / último como padrão, mas acho que a maioria dos desenvolvedores de pacotes terá suas funções aceitando dados como primeiro argumento - e os argumentos opcionais? +1 para algo como

1 |> sin |> sum(2, _)

Como foi mencionado anteriormente, a legibilidade e a simplicidade são muito importantes. Eu não gostaria de perder todo o estilo dplyr / tidyverse de fazer coisas para análise de dados ...

Gostaria de acrescentar que também considero muito útil a sintaxe multilinha do Elixir para o operador de pipe.

1
|> sin
|> sum(2)
|> println

É o equivalente a println(sum(sin(1),2))

Apenas para observar uma proposta no mundo javascript . Eles usam o operador ? em vez de _ ou ~ qual já significa ( _ para ignorar algo e ~ bit a bit não ou formular). Dado que atualmente usamos ? da mesma forma que javascript, podemos usá-lo para o marcador de posição currying também.

é assim que sua proposta se parece (está em javascript, mas também é válida em julia :)

const addOne = add(1, ?); // apply from the left
addOne(2); // 3

const addTen = add(?, 10); // apply from the right
addTen(2); // 12

// with pipeline
let newScore = player.score
  |> add(7, ?)
  |> clamp(0, 100, ?); // shallow stack, the pipe to `clamp` is the same frame as the pipe to `add`.

const maxGreaterThanZero = Math.max(0, ...);
maxGreaterThanZero(1, 2); // 2
maxGreaterThanZero(-1, -2); // 0

Um resumo porque comecei a escrever um por outros motivos.
Veja também minha lista anterior pacotes relacionados .

Qualquer mexer com _ não quebra e pode ser feito em 1.x, porque https://github.com/JuliaLang/julia/pull/20328

Tudo isso se resume a duas opções principais (além do status quo).
Ambos podem (para todos os efeitos) ser implementados com uma macro para reescrever a sintaxe.

Mexendo com _ para criar funções anônimas

@StefanKarpinski 's Terse Lambdas , ou sintaxe semelhante onde a presença de um _ (em uma expressão RHS), indica que toda aquela expressão é uma função anônima.

  • isso quase pode

    • A única coisa que não pode ser feita é (_) não ser o mesmo que _ . Que é apenas a função de identidade, então realmente não importa

    • Isso se aplicaria em qualquer lugar, então não seria útil apenas com |> , mas também, por exemplo, com escrever coisas compactamente como map(log(7,_), xs) ou log(7, _).(xs) para obter o log com base 7 de cada elemento de xs .

    • Pessoalmente, sou a favor disso, se estivéssemos fazendo alguma coisa.

Mexendo com |> para fazê-lo realizar substituições

As opções incluem:

  • Faça seu RHS agir como se curry

    • na verdade, acho que isso está quebrando (embora talvez haja uma versão não quebrável que verifica a tabela de métodos. Eu acho que isso é apenas confuso)

  • Faça com que _ act especial (veja as opções acima e / ou várias maneiras de falsificar por meio de reescrita)

    • uma maneira de fazer isso seria permitir a criação de macros infixas, em seguida, alguém poderia escrever @|>@ e defini-lo como quiser em pacotes (isso já foi fechado uma vez https://github.com/JuliaLang/julia/ questões / 11608)

    • ou dar a ele essas propriedades especiais intrinsecamente

  • Temos toneladas de implementações de macro para fazer isso, como eu disse, veja minha lista de pacotes relacionados
  • Algumas pessoas também propõem alterá-lo para torná-lo (ao contrário de todos os outros operadores) capaz de fazer com que uma expressão na linha antes que ocorra não termine. Então você pode escrever
a
|> f
|>g

Em vez do atual:

a |>
f |>
g

(Implementar isso com uma macro não é possível sem colocar o bloco entre colchetes. E colocar o bloco entre colchetes faz com que funcione de qualquer maneira)

  • Eu pessoalmente não gosto dessas propostas, pois elas tornam |> (uma operadora já detestada) super mágica.

Edit : como @StefanKarpinski aponta abaixo , isso sempre interrompe as alterações.
Porque alguém pode estar dependendo de typeof(|>) <: Function .
E essas mudanças o tornariam um elemento da sintaxe da linguagem.

Opção bônus: nunca está acontecendo. Opção: adicione currying em todos os lugares # 554

É muito tarde na língua para adicionar currying.
Seria uma quebra louca, adicionando enormes pilhas de ambigüidades em todos os lugares.
E simplesmente ser muito confuso.

Acho que com essas duas opções, basicamente cobre tudo que vale a pena considerar.
Eu não acho que mais nada esclarecedor foi dito (ou seja, sem contar "+1 quero isso"; ou repetições de microvarientes acima).

Estou bastante tentado a descontinuar |> em 0,7 para que possamos apresentá-lo posteriormente com uma semântica mais útil e possivelmente não funcional, que suspeito ser necessária para fazer o encanamento funcionar bem.

Estou bastante tentado a descontinuar |> em 0.7 para que possamos apresentá-lo posteriormente com uma semântica mais útil e possivelmente não funcional, que suspeito ser necessária para fazer o encanamento funcionar bem.

O único caso de quebra nessa lista é quando |> faz seu lado direito fingir que está currying.
Para inserir seus argumentos na primeira ou na última posição do argumento (/ s), ou em alguma outra posição fixa (a segunda pode fazer sentido).
Sem usar _ como um marcador para qual argumento inserir.

Nenhuma outra proposta de quebra foi feita que alguém neste tópico levou vagamente a sério.
Eu ficaria surpreso se houvesse outras definições sensatas, mas ainda assim incompletas, para essa operação,
que ninguém sugeriu ainda nestes últimos quase 4 anos.

De qualquer forma, depreciar não seria terrível.
Os pacotes que o usam ainda podem tê-lo por meio de um dos pacotes de macro.

Outra ideia pode ser manter |> com o comportamento atual e introduzir a nova funcionalidade com um nome diferente que não requeira o uso da tecla shift, como \\ (que não até mesmo analise como um operador agora). Conversamos sobre isso uma vez no Slack, mas acho que a história provavelmente se perdeu nas areias do tempo.

A tubulação é frequentemente usada de forma interativa e a facilidade de digitação pelo operador afeta a sensação de "leveza" do uso. Um único caractere | pode ser bom.

Um único caractere | pode ser bom.

Sim, interativamente, mas é o suficiente para tê-lo em .juliarc.jl (que eu tenho há muito tempo ;-p)

que não requer o uso da tecla shift

Observe que esta é uma propriedade altamente dependente da localidade. Por exemplo, meu teclado sueco enviou uma série de caracteres para mudar e combinações de AltGr (bastante horríveis) para dar espaço para outras três letras.

Existe alguma tradição de usar |> para este propósito? [Mathematica] (http://reference.wolfram.com/language/guide/Syntax.html) tem // para o aplicativo de função postfix, que deve ser fácil de digitar na maioria dos teclados e pode estar disponível, se estiver já não é usado para comentários (como em C ++) ou divisão de inteiros (como em Python).

Algo com | nele tem uma boa conexão com script de shell, embora se fosse esperado que um único | fosse bit a bit OR. || considerado OR lógico? E quanto a ||| ? Digitar um caractere difícil de alcançar três vezes não é muito mais difícil do que digitar uma vez.

Existe alguma tradição de usar |> para este propósito?

Eu acredito que a tradição de |> deriva da família de linguagens ML. Quando se trata de operadores, poucas comunidades de linguagens de programação exploraram esse espaço como a comunidade ML / Haskell. Uma pequena seleção de exemplos:

Para adicionar à lista acima, R usa%>% - e embora essa linguagem seja desatualizada, acho que sua funcionalidade de pipe é muito bem projetada. Uma das coisas que o torna eficaz é a sintaxe da chave, que permite escrever coisas como

x %>% { if(. < 5) { a(.) } else { b(.) } }

o que seria um pouco mais prolixo em Julia devido ao uso de declarações finais. Embora meu exemplo acima seja abstrato, muitas pessoas usam sintaxe semelhante ao escrever código de pré-processamento de dados. Com qualquer uma das propostas atuais sendo discutidas, algo semelhante ao acima pode ser alcançado - talvez através do uso de parênteses?

Eu também encorajaria o uso de um operador pipe que forneça alguma indicação visual de que os argumentos estão sendo encaminhados, como o símbolo > . Isso fornece uma dica útil para iniciantes e aqueles não familiarizados com a programação funcional.

Mesmo que os usos propostos de |> não sejam incompatíveis com o uso típico atual sintaticamente, eles _são_ incompatíveis com |> sendo um operador - já que a maioria deles envolve dar |> muito mais poder do que uma mera função infixo. Mesmo se tivermos certeza de que queremos reter x |> f |> g para significar g(f(x)) , deixá-lo como um operador normal provavelmente impedirá qualquer outro aprimoramento. Embora mudar |> em um não-operador que executa a aplicação de função postfix possa não quebrar seu uso _típico_ para o aplicativo de função encadeada, ainda não seria permitido, pois quebraria o uso _atípico_ de |> - qualquer coisa que depende de ser um operador. Interromper o uso atípico ainda é interrompido e, portanto, não é permitido nas versões 1.x. Se quisermos fazer qualquer uma das propostas acima com |> , pelo que posso dizer, precisamos fazer |> sintaxe em vez de uma função em 1.0.

@StefanKarpinski está fazendo |> sintaxe em vez de uma função mesmo na mesa no momento? É possível colocá-lo na mesa a tempo de colocá-lo no lugar para 1.0?

@StefanKarpinski está criando |> sintaxe em vez de uma função mesmo na mesa no momento? É possível colocá-lo na mesa a tempo de colocá-lo no lugar para 1.0?

Descontinuá-lo em 0.7 e removê-lo completamente de 1.0 está na mesa.
Em seguida, traga-o de volta algum tempo durante 1.x como um elemento de sintaxe.
O que, naquele ponto, seria uma mudança ininterrupta.

Alguém precisaria fazer isso, mas não acho que seja uma mudança terrivelmente difícil, então sim, está sobre a mesa.

Para o que |> seria preterido? Uma implementação em Lazy.jl?

x |> f pode ser preterido para f(x) .

Que tal descontinuar l> mas ao mesmo tempo introduzir, digamos, ll> que tem o mesmo comportamento do l> atual?

Se seguirmos apenas com a depreciação sem alguma substituição, os pacotes que dependem do comportamento atual ficariam essencialmente sem uma boa opção. Se eles obtiverem uma expressão que seja um pouco menos agradável nesse meio tempo, eles podem continuar com seu design atual, mas ainda deixamos a opção de encontrar uma solução realmente boa para l> no futuro.

Isso afeta o ecossistema Query and friends de uma maneira grande: eu criei um sistema que é bastante semelhante à sintaxe do pipe no R tidyverse. A coisa toda é bastante abrangente: cobre o arquivo io para atualmente sete formatos de arquivo tabular (com mais dois muito próximos), todas as operações de consulta (como dplyr) e plotagem (não muito longe, mas estou otimista de que podemos algo que parece ggplot em breve). Tudo se baseia na implementação atual de l> ...

Devo dizer que sou totalmente a favor de manter as opções de algo melhor por l> na mesa. Funciona bem para o que criei até agora, mas posso ver facilmente uma abordagem melhor. Mas apenas depreciar parece um passo muito radical que pode arrancar o tapete de muitos pacotes.

A outra escolha é apenas fazer de x |> f uma sintaxe alternativa para f(x) . Isso quebraria o código que sobrecarrega |> mas permite que o código que usa |> para o encadeamento de funções continue funcionando, deixando as coisas abertas para melhorias adicionais, desde que sejam compatíveis com isso.

A alternativa seria introduzir uma nova sintaxe de encadeamento sintático no futuro, mas ela precisa ser algo que atualmente é um erro de sintaxe, o que é uma escolha muito pequena neste ponto.

A alternativa seria introduzir uma nova sintaxe de encadeamento sintático no futuro, mas ela precisa ser algo que atualmente é um erro de sintaxe, o que é uma escolha muito pequena neste ponto.

Minha proposta de cima não permitiria isso? Ou seja, tornar |> um erro de sintaxe em julia 1.0 e tornar ||> equivalente ao |> . Para julia 1.0, isso seria um pequeno aborrecimento para o código que atualmente usa |> porque seria necessário alternar para ||> . Mas acho que não seria tão ruim, além disso, poderia ser totalmente automatizado. Então, quando alguém tem uma boa ideia por |> ela pode ser reintroduzida na linguagem. Nesse ponto, haveria ||> e |> ao redor, e presumo que ||> desapareceria lentamente no fundo se todos começarem a adotar |> . E então, em alguns anos, o julia 2.0 poderia simplesmente remover ||> . Em minha mente, isso a) não causaria nenhum problema real a ninguém no período julia 1.0 eb) deixaria todas as opções sobre a mesa para uma solução realmente boa por |> eventualmente.

|>(x, f) = f(x)
|>(x, tuple::Tuple) = tuple[1](x, tuple[2:endof(tuple)]...) # tuple
|>(x, f, args...) = f(x, args...) # args

x = 1 |> (+, 1, 1) |> (-, 1) |> (*, 2) |> (/, 2) |> (+, 1) |> (*, 2) # tuple
y = 1 |> (+, 1, 1)... |> (-, 1)... |> (*, 2)... |> (/, 2)... |> (+, 1)... |> (*, 2)... # args

Não é fácil escrever muitas vezes, mas da esquerda para a direita e não usa macro.

function fibb_tuple(n)
    if n < 3
        return n
    end
    fibb_tuple(n-3) |> (+, fibb_tuple(n-2), fibb_tuple(n-1))
end

function fibb_args(n)
    if n < 3
        return n
    end
    fibb_args(n-3) |> (+, fibb_args(n-2), fibb_args(n-1))...
end

function fibb(n)
    if n < 3
        return n
    end
    fibb(n-3) + fibb(n-2) + fibb(n-1)
end

n = 25

println("fibb_tuple")
<strong i="8">@time</strong> fibb_tuple(1)
println("fibb_args")
<strong i="9">@time</strong> fibb_args(1)
println("fibb")
<strong i="10">@time</strong> fibb(1)

println("tuple")
<strong i="11">@time</strong> fibb_tuple(n)
println("args")
<strong i="12">@time</strong> fibb_args(n)
println("fibb")
<strong i="13">@time</strong> fibb(n)
fibb_tuple
  0.005693 seconds (2.40 k allocations: 135.065 KiB)
fibb_args
  0.003483 seconds (1.06 k allocations: 60.540 KiB)
fibb
  0.002716 seconds (641 allocations: 36.021 KiB)
tuple
  1.331350 seconds (5.41 M allocations: 151.247 MiB, 20.93% gc time)
args
  0.006768 seconds (5 allocations: 176 bytes)
fibb
  0.006165 seconds (5 allocations: 176 bytes)

|>(x, tuple::Tuple) = tuple[1](x, tuple[2:endof(tuple)]...) é horrível.
|>(x, f, args...) = f(x, args...) precisa de mais cartas, mas rápido.

Acho que permitir a sintaxe subject |> verb(_, objects) like como verb(subject, objects) significa suportar SVO (mas o padrão de Julia é VSO ou VOS). No entanto, Julia apóia o mutltidipatch para que o assunto possa ser assuntos. Acho que devemos permitir a sintaxe (subject1, subject2) |> verb(_, _, object1, object2) like como verb(subject1, subject2, object1, object2) se introduzirmos a sintaxe SVO.
É MIMO se for entendido como pipeline, como @oxinabox observou.

Que tal usar (x)f como f(x) ?
(x)f(y) pode ser lido como f(x)(y) e f(y)(x) portanto, escolha primeiro avaliar corretamente:

(x)f # f(x)
(x)f(y) # f(y)(x)
(x)f(y)(z) # f(y)(z)(x)
(x)(y)f(z) # f(z)(y)(x)
(a)(b)f(c)(d) # f(c)(d)(b)(a)
1(2(3, 4), 5(6, 7), 8(9, 10)) # 1(2(3, 4), 5(6, 7), 8(9, 10))
1 <| (2 <| (3, 4), 5 <| (6, 7), 8 <| (9, 10)) # 1(2(3, 4), 5(6, 7), 8(9, 10)), but 2 <| (3, 4) == 2((3, 4)) so currently emit error
3 |> 2(_, 4) |> 1(_, 5(6, 7), 8(9, 10)) # 1(2(3, 4), 5(6, 7), 8(9, 10))
((3)2(_, 4))1(_, 5(6, 7), 8(9, 10)) # 1(2(3, 4), 5(6, 7), 8(9, 10))
(3, 4) |> 2(_, _) |> 1(_, 5(6, 7), 8(9, 10)) # 1(2(3, 4), 5(6, 7), 8(9, 10))
((3, 4)2)1(_, 5(6, 7), 8(9, 10)) # 1(2(3, 4), 5(6, 7), 8(9, 10))

Isso pode manipular vararg claramente.
Mas vai quebrar operadores binários sem espaços:

(a + b)+(c + d) # +(c + d)(a + b) == (c + d)(a + b): Error

Opção alternativa: adicione outro caso para a sintaxe de splatting. Faça f ... (x) desugar para (args ...) -> f (x, args ...)

Isso permitiria currying sintaticamente leve (manual):

#Basic example:
f(a,b,c,d) = #some definition
f...(a)(b,c,d) == f(a,b,c,d)
f...(a,b)(c,d) == f(a,b,c,d)
f...(a,b,c)(d) == f(a,b,c,d)
f...(a)...(b)(c,d) == f(a,b,c,d) # etc etc

# Use in pipelining:
x |> map...(f) |> g  |> filter...(h) |> sum

Quando você para de curry? As funções de Julia não têm aridade fixa.

Julia já tem um operador de splatting. O que estou propondo teria exatamente o mesmo comportamento do operador splat atual.

I, e: f ... (x) == (args ...) -> f (x, args ...) é o açúcar para fazer um lambda com splatting.

Essa definição sempre fornece um objeto de função. Presumivelmente, às vezes você quer uma resposta.

Você consegue isso chamando explicitamente o objeto no final. Por exemplo, observe a falta de ... antes do último conjunto de parênteses em meu último exemplo f ... (a) ... (b) (c, d) == f (a, b, c, d) .

Você também pode chamar o objeto de função retornado com |>, o que o torna bom para tubulação.

@saolof

Exemplo básico:

f (a, b, c, d) = #alguma definição
f ... (a) (b, c, d) == f (a, b, c, d)
f ... (a, b) (c, d) == f (a, b, c, d)
f ... (a, b, c) (d) == f (a, b, c, d)
f ... (a) ... (b) (c, d) == f (a, b, c, d) # etc etc

Boa intuição para usar splat com encadeamento de funções, mas muito complexo no meu sentido.
Você fez várias inscrições em um ponto da cadeia.
No encadeamento de funções, você faz um aplicativo passo a passo ao longo de toda a cadeia.

E @StefanKarpinski está certo, você não sabe quando parar para aplicar funções sobre si mesmas e finalmente aplicá-las a um item mais escalar.

- (cortado) -

Desculpe, isso é bastante inútil e ilegível.
Veja minha segunda mensagem abaixo para obter uma explicação mais clara (espero).

Dado o quão funcional Julia já é, eu gosto bastante da ideia de @saolof de um operador de função-curry. Eu realmente não entendo de onde vêm as objeções semânticas, pois parece que esta tem uma interpretação muito óbvia.

Você pode até mesmo prototipá-lo do conforto do seu próprio repl:

ctranspose(f) = (a...) -> (b...) -> f(a..., b...)

map'(+)(1:10)

map'(+)'(1:10, 11:20)(21:30)

(+)'(1,2,3)(4,5)

1:10 |> map'(x->x^2) |> filter'(iseven)

Tem uma sensação agradável, eu acho.

Edit: Parece que este também pode ser o caminho para generalizar isso mais. Se pudermos escrever map∘(+, 1:10) então podemos escrever map∘(_, 1:10) para colocar primeiro o argumento curry, e o operador de curry determina o escopo do lambda, resolvendo o maior problema para esse currying geral.

Eh, isso é inteligente, @MikeInnes.

Eu amo como a extensibilidade extrema de Julia se mostra aqui também. A solução unificadora para uma ampla gama de requisitos para o encadeamento de funções acaba por abusar de ctranspose ...

(esclarecimento: estou recebendo 1:10 |> map'(x->x^2) |> filter'(iseven) com esta proposta, então estou 💯% a favor!)

Para ser claro, não acho que devamos abusar do operador adjoint para isso, mas é uma boa prova de conceito que podemos ter uma notação de currying de função concisa.

Talvez devêssemos introduzir um novo operador Unicode? http://www.fileformat.info/info/unicode/char/1f35b/index.htm

(Desculpa...)

Eu sinto que _'s ainda é uma maneira muito mais flexível de fazer lambdas

@bramtayl Eu acho que a ideia na edição de MikeInnes para sua postagem é que os dois podem coexistir - sublinhados autônomos como em @stevengj a solicitação de pull funcionaria, currying autônomo como na ideia de Mike acima funcionaria e combinando os dois também funcionaria, permitindo que você use o operador currying para delimitar o escopo de _ s dentro dele.

eu entendi

Isso não o torna muito diferente do LazyCall.jl

Em uma nota mais grave:

Para ser claro, não acho que devamos abusar do operador adjoint por isso

Provavelmente uma escolha acertada. No entanto, gostaria de expressar minha esperança de que, se tal solução for implementada, ela receba um operador fácil de digitar. A capacidade de fazer algo como 1:10 |> map'(x->x^2) é significativamente menos útil se qualquer caractere que substituir ' requer que eu procure em uma tabela unicode (ou use um editor que suporte expansões LaTeX).

Em vez de abusar do operador adjunto, poderíamos reutilizar o splat.

  • em um contexto de tubulação (linear)
  • dentro, em uma chamada de função

    • faça splat antes do que depois

então

  • splat pode induzir um argumento iterador ausente

Uma espécie de splat de alta ordem (com anacrusis se houver algum músico lá).
Esperando que não abale muito a língua.

EXEMPLO

1:10
    |> map(...x->x^2)
    |> filter(...iseven)

EXEMPLO 2

genpie = (r, a=2pi, n=12) ->
  (0:n-1) |>
      map(...i -> a*i/n) |>
      map(...t -> [r*cos(t), r*sin(t)]) 

poderia representar

elmap = f -> (s -> map(f,s))

genpie = (r, a=2pi, n=12) ->
  (0:n-1) |>
      elmap(i -> a*i/n) |>
      elmap(t -> [r*cos(t), r*sin(t)]) 

Não tenho certeza se isso pertence aqui, uma vez que a discussão evoluiu para um encadeamento e sintaxe mais avançados / flexíveis ... mas voltando ao post de abertura, o encadeamento de funções com sintaxe de ponto parece possível agora, com um pouco de configuração extra. A sintaxe é apenas uma consequência de ter sintaxe de ponto para estruturas junto com funções / fechamentos de primeira classe.

mutable struct T
    move
    scale
    display
    x
    y
end

function move(x,y)
    t.x=x
    t.y=y
    return t
end
function scale(c)
    t.x*=c
    t.y*=c
    return t
end
function display()
    @printf("(%f,%f)\n",t.x,t.y)
end

function newT(x,y)
    T(move,scale,display,x,y)
end


julia> t=newT(0,0)
T(move, scale, display, 0, 0)

julia> t.move(1,2).scale(3).display()
(3.000000,6.000000)

A sintaxe parece muito semelhante à OOP convencional, com uma peculiaridade de "métodos de classe" serem mutáveis. Não tenho certeza de quais são as implicações de desempenho.

@ivanctong O que você descreveu é algo mais parecido com uma interface fluente do que um encadeamento de funções.

Dito isso, resolver o problema do encadeamento de funções de maneira mais geral teria o benefício adicional de também ser utilizável para interfaces fluentes. Embora seja certamente possível fazer algo como uma interface fluente usando membros de estrutura em Julia atualmente, me parece que vai contra o espírito e a estética de design de Julia.

A forma como o elixir faz isso, onde o operador de tubo sempre passa no lado esquerdo como o primeiro argumento e permite argumentos extras depois, tem sido muito útil, eu adoraria ver algo como "elixir" |> String.ends_with?("ixir") como um cidadão de primeira classe em Julia.

Outros idiomas a definem como Sintaxe de chamada de função uniforme .
Este recurso oferece várias vantagens (veja a Wikipedia), seria bom se Julia o apoiasse.

Então, há uma interface fluente para Julia neste momento?

Por favor, poste perguntas no fórum de discussão do discurso de Julia .

Em um ataque de hacking (e julgamento questionável !?) Eu criei outra solução possível para a rigidez da vinculação de marcadores de posição de função:

https://github.com/c42f/MagicUnderscores.jl

Conforme observado em https://github.com/JuliaLang/julia/pull/24990 , isso é baseado na observação de que muitas vezes se deseja que determinados slots de uma determinada função vinculem uma expressão de marcador _ firmemente e outros vagamente. MagicUnderscores torna isso extensível para qualquer função definida pelo usuário (muito no espírito da máquina de transmissão). Assim, podemos ter coisas como

julia> <strong i="12">@_</strong> [1,2,3,4] |> filter(_>2, _)
2-element Array{Int64,1}:
 3
 4

julia> <strong i="13">@_</strong> [1,2,3,4] |> filter(_>2, _) |> length
2

"apenas trabalhe". (Com o @_ obviamente indo embora, se for possível tornar isso uma solução geral.)

Alguma sugestão de variação do @MikeInnes parece adequada para minhas necessidades (geralmente longas cadeias de filtrar, mapear, reduzir, enumerar, zipar etc. usando a sintaxe do ).

c(f) = (a...) -> (b...) -> f(a..., b...)

1:10 |> c(map)() do x
    x^2
end |> c(filter)() do x
    x > 50
end

Isso funciona, embora eu não consiga mais fazer ' funcionar. É um pouco mais curto que:

1:10 |> x -> map(x) do x
    x^2
end |> x -> filter(x) do x
    x > 50
end

Também acho que se poderia simplesmente fazer

cmap = c(map)
cfilter = c(filter)
cetc = c(etc)
...

1:10 |> cmap() do x
    x^2
end |> cfilter() do x
    x > 50
end |> cetc() do ...

A partir de 1.0, você precisará sobrecarregar adjoint vez de ctranspose . Você também pode fazer:

julia> Base.getindex(f::Function, x...) = (y...) -> f(x..., y...)

julia> 1:10 |> map[x -> x^2] |> filter[x -> x>50]
3-element Array{Int64,1}:
  64
  81
 100

Se pudéssemos sobrecarregar apply_type , poderíamos obter map{x -> x^2} :)

@MikeInnes acabei de roubar isso

Uma contribuição tardia e ligeiramente frívola - que tal enviar dados para qualquer local na lista de argumentos usando uma combinação de operadores curry esquerdo e direito:

VERSION==v"0.6.2"
import Base: ctranspose, transpose  
ctranspose(f::Function) = (a...) -> ((b...) -> f(a..., b...))  
 transpose(f::Function) = (a...) -> ((b...) -> f(b..., a...))

"little" |> (*)'''("Mary ")("had ")("a ") |> (*).'(" lamb")

Clojure tem algumas macros de threading interessantes . Temos aqueles no ecossistema Julia em algum lugar?

Clojure tem algumas macros de threading interessantes . Temos aqueles no ecossistema Julia em algum lugar?

https://github.com/MikeInnes/Lazy.jl

Clojure tem algumas macros de threading interessantes . Temos aqueles no ecossistema Julia em algum lugar?

temos pelo menos 10 deles.
Postei uma lista mais adiante no tópico.
https://github.com/JuliaLang/julia/issues/5571#issuecomment -205754539

Você pode editar a lista para ter LightQuery em vez dos outros dois pacotes meus?

Já que o operador |> vem do elixir, por que não se inspirar em uma das maneiras que eles têm de criar funções anônimas?
no elixir você pode usar &expr para definir uma nova função anônima e &n para capturar argumentos posicionais ( &1 são os primeiros argumentos, &2 é o segundo, etc.)
No elixir, há coisas extras para escrever (por exemplo, você precisa de um ponto antes do parêntese para chamar uma função anônima &(&1 + 1).(10) )

Mas aqui está o que poderia ser em julia

&(&1 * 10)        # same as: v -> v * 10
&(&2 + 2*&5)      # same as: (_, x, _, _, y) -> x + 2*y
&map(sqrt, &1)    # same as: v -> map(sqtr, v)

Portanto, podemos usar o operador |> mais adequada

1:9 |> &map(&1) do x
  x^2
end |> &filter(&1) do x
  x in 25:50
end

ao invés de

1:9 |> v -> map(v) do x
  x^2
end |> v -> filter(v) do x
  x in 25:50
end

observe que você pode substituir as linhas 2 e 3 por .|> &(&1^2) ou .|> (v -> v^2)

A principal diferença com as proposições com _ placeholder é que aqui é possível usar argumentos posicionais, e & na frente das expressões torna o escopo dos placeholders óbvio (para o leitor e o compilador).

Observe que peguei & em meus exemplos, mas o uso de ? , _ , $ ou qualquer outra coisa, não mudaria nada para o caso.

Scala usa _ para o primeiro argumento, _ para o segundo argumento, etc., que é conciso, mas você rapidamente fica sem situações onde pode aplicá-lo (não pode repetir ou reverter a ordem dos argumentos). Também não possui um prefixo ( & na sugestão acima) que desambigua funções de expressões, e isso, na prática, é outra questão que impede seu uso. Como um praticante, você acaba envolvendo as funções embutidas pretendidas em parênteses extras e chaves, na esperança de que sejam reconhecidas.

Portanto, eu diria que a maior prioridade ao introduzir uma sintaxe como essa é que ela não seja ambígua.

Mas quanto aos prefixos para argumentos, $ tem uma tradição no mundo do shell scripting. É sempre bom usar personagens familiares. Se |> é do Elixir, então isso poderia ser um argumento para tirar & do Elixir também, com a idéia de que os usuários já estão pensando nesse modo. (Supondo que haja muitos ex-usuários do Elixir por aí ...)

Uma coisa que uma sintaxe como esta provavelmente nunca pode capturar é a criação de uma função que recebe N argumentos, mas usa menos que N. O $1 , $2 , $3 no corpo implica a existência de 3 argumentos, mas se você quiser colocar isso em uma posição onde será chamado com 4 argumentos (o último a ser ignorado), então não há uma forma natural de expressá-lo. (Além de predefinir funções de identidade para cada N e envolver a expressão com um deles.) Isso não é relevante para o caso motivador de colocá-lo após |> , que tem apenas um argumento, no entanto.

Eu estendi o truque @MikeInnes de sobrecarregar getindex, usando Colon como se as funções fossem arrays:

struct LazyCall{F} <: Function
    func::F
    args::Tuple
    kw::Dict
end

Base.getindex(f::Function,args...;kw...) = LazyCall{typeof(f)}(f,args,kw)

function (lf::LazyCall)(vals...; kwvals...)

    # keywords are free
    kw = merge(lf.kw, kwvals)

    # indices of free variables
    x_ = findall(x->isa(x,Colon),lf.args)
    # indices of fixed variables
    x! = setdiff(1:length(lf.args),x_)

    # the calling order is aligned with the empty spots
    xs = vcat(zip(x_,vals)...,zip(x!,lf.args[x!])...)
    args = map(x->x[2],sort(xs;by=x->x[1]))

    # unused vals go to the end
    callit = lf.func(args...,vals[length(x_)+1:end]...; kw...)

    return callit
end

[1,2,3,4,1,1,5]|> replace![ : , 1=>10, 3=>300, count=2]|> filter[>(50)]  # == [300]

log[2](2) == log[:,2](2) == log[2][2]() == log[2,2]()  # == true

É muito mais lento que lambdas ou macros de threading, mas acho super legal: p

Para lembrar as pessoas que comentam aqui, dê uma olhada na discussão relevante em https://github.com/JuliaLang/julia/pull/24990.

Além disso, eu encorajo você a experimentar https://github.com/c42f/Underscores.jl que fornece uma implementação amigável de encadeamento de funções da sintaxe _ para espaços reservados. @jpivarski com base em seus exemplos, você pode achar que é bastante familiar e confortável.

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