Julia: A transmissão teve um trabalho (por exemplo, transmissão por iteradores e gerador)

Criado em 21 set. 2016  ·  69Comentários  ·  Fonte: JuliaLang/julia

Foi surpreendente descobrir que broadcast não está funcionando com iteradores

dict = Dict(:a => 1, :b =>2)
<strong i="7">@show</strong> string.(keys(dict)) # => Expected ["a", "b"]
"Symbol[:a,:b]"

Isso ocorre porque Broadcast.containertype retornou Any https://github.com/JuliaLang/julia/blob/413ed79ec54f3a754ac8bc57c1d29835d17bd274/base/broadcast.jl#L31
levando ao fallback em: https://github.com/JuliaLang/julia/blob/413ed79ec54f3a754ac8bc57c1d29835d17bd274/base/broadcast.jl#L265

Definir containertype como Array para esse iterador leva a problemas ao chamar size nele, porque broadcast não verifica a interface do iterador iteratorsize(IterType) .

map resolve isso com o substituto map(f, A) = collect(Generator(f,A)) que pode ser mais sensível que a definição atual de broadcast(f, Any, A) = f(A)

broadcast

Comentários muito úteis

Isso é intencional. broadcast é para contêineres com formas e o padrão para tratar objetos como escalares. map é para contêineres sem formas, e o padrão para tratar objetos como iteradores.

Por exemplo, broadcast trata strings como "escalares", enquanto map itera sobre os caracteres.

Todos 69 comentários

Isso é intencional. broadcast é para contêineres com formas e o padrão para tratar objetos como escalares. map é para contêineres sem formas, e o padrão para tratar objetos como iteradores.

Por exemplo, broadcast trata strings como "escalares", enquanto map itera sobre os caracteres.

Talvez o problema seja que as pessoas acham a nova sintaxe de pontos muito conveniente. No entanto, existia o desejo de se ter uma forma compacta de expressar map . Infelizmente, a sintaxe de ponto já foi usada.

Além disso, como @stevengj apontou antes: deve haver uma diferença entre map e broadcast , se não, qual é o sentido de ter ambos.

@stevengj Mas os Iteradores têm forma (especialmente geradores) http://docs.julialang.org/en/release-0.5/manual/interfaces/#interfaces

Eu diria que os iteradores estão neste domínio estranho onde a maioria das coisas que você gostaria de fazer com um contêiner que você também deseja fazer com iteradores, e sim, talvez seja puramente o fato de que a sintaxe . é muito conveniente (e o erro que você obtém é muito opaco).

@pabloferz A principal diferença entre map e broadcast é o tratamento dos escalares. Agora, a definição de escalar é discutível e eu diria que tudo que tem length(x) > 1 não deve ser considerado um escalar.

Marcar quais argumentos devem ser tratados como iteráveis, em vez da própria chamada de função, removeria a ambigüidade. Eu penso?

Para broadcast (acredito que também em geral) ter forma significa ter size (não apenas length ) e ser indexável. Exceto para tuplas, qualquer coisa sem size é tratada como escalar. Dada a implementação atual, você primeiro precisa de getindex ou ser capaz de definir uma para o objeto que deseja transmitir. Para iteradores, isso não é possível em geral.

Eu também corri para isso. Vindo de # 16769, onde procuro uma maneira de fill! um array com avaliações repetidas de uma função (em vez de um valor fixo), pensei que a sintaxe de ponto já pode resolver o problema. Mas, quando a = zeros(2, 3); a .= [rand() for i=1:2, j=1:3] funciona, o (seria) mais barato a .= (rand() for i=1:2, j=1:3) não funciona; este gerador é HasShape() , mas de fato não tem capacidade de indexação. Não entendi muito bem como a sintaxe de transmissão / ponto funciona, mas seria útil aqui ter uma característica para recursos de indexação? já existe um PR (# 22489) para isso ...

@rfourquet , você pode fazer a = zeros(2, 3); a .= rand.()

Sim, mas deveria ter sido mais preciso: quero usar uma função que obtenha os índices como parâmetros, como a .= (f(i, j) for i=1:2, j=1:3) .

Quais seriam as desvantagens das dimensões de transmissão de HasShape iteradores? Parece uma coisa natural a se fazer.

@nalimilan , à primeira vista acho que seria razoável e provavelmente relativamente fácil de implementar. Estaria quebrando então deve ser feito por 1.0.

Um problema potencial com isso é que HasShape iteradores não suportam necessariamente getindex , e isso pode dificultar a implementação?

Uma possibilidade seria temporariamente (para 1.0) fazer uma implementação simples que acabou de ser copiada para um array. Isso permitiria uma otimização pós 1.0

Um problema potencial com isso é que os iteradores HasShape não suportam necessariamente getindex, e isso pode dificultar a implementação?

Como eu disse acima, tenho um PR em # 22489 para permitir a indexação em iteradores, se isso puder ajudar.

O que precisa ser feito no 1.0 para que pelo menos possamos melhorar o comportamento no 1.x?

Obrigado @nalimilan por trazer isso à tona, eu também queria fazer isso. Se não for possível implementar HasShape geradores no lado direito da expressão de broadcast para 1.0, devemos cometer esse erro agora, em vez de tratar os geradores como escalares? para que isso possa ser ativado em 1.x.

: +1: A triagem recomenda errar (a escolha segura) ou ligar para collect (se for fácil de fazer).

map trata todos os seus argumentos como contêineres e tenta iterar sobre todos eles. No meu mundo ideal, broadcast seria semelhante, e trataria todos os seus argumentos tendo formas que podem ser transmitidas, e forneceria um erro se, por exemplo, size não fosse definido. Vou apontar que qualquer valor pode ser tratado como um escalar na transmissão, envolvendo-o com fill , resultando em uma matriz 0-d:

julia> fill("a")
0-dimensional Array{String,0}:
"a"

julia> fill([2])
0-dimensional Array{Array{Int64,1},0}:
[2]

Você realmente sugere tratar todos os escalares como contêineres por padrão? Isso não parece muito prático.

Observando como poderíamos oferecer suporte a qualquer iterável ou apenas lançar um erro para eles até que os suportemos, parece que precisaríamos de uma maneira de identificar os iteradores em BroadcastStyle . Atualmente isso não é possível, uma vez que Base.iteratorsize retorna HasLength mesmo para escalares como Symbol . Poderíamos introduzir um traço Base.isiterable (que poderia ser útil para outras coisas), ou tornar Base.iteratorsize NotIterable padrão (o que faria sentido também como tendo HasLength como padrão sempre parece um pouco surpreendente, embora inofensivo).

(Caso complicado para discussão futura: UniformScaling .)

@timholy Desde que você fez o redesenho de broadcast , alguma sugestão?

@JeffBezanson , o objetivo de broadcast é ser capaz de "transmitir" escalares para corresponder aos contêineres, por exemplo, fazer ["bug", "cow", "house"] .* "s" ----> ["bugs", "cows", "houses"] . Isso é fundamentalmente diferente do comportamento de map .

É por isso que broadcast trata objetos como escalares por padrão, para que possam ser combinados com contêineres. Lançar um erro para um tipo não reconhecido o tornaria muito menos útil.

Deve ser possível declarar um tipo específico como um contêiner para broadcast definindo algum método apropriado nele, mas acho que o padrão deve continuar a ser tratar objetos como escalares.

Em um PR não relacionado (https://github.com/JuliaLang/julia/pull/25339), @Keno sugeriu usar applicable(start, (x,)) para descobrir se x é iterável ou não. Devemos usar a mesma abordagem aqui? Eu acharia mais claro ter uma definição mais explícita de iteradores (com base em Base.iteratorsize ou em uma característica), mas usar start também faz sentido.

Poderíamos ter uma característica explícita cujo padrão é applicable(start, (x,)) ; isso permitiria substituí-lo, se necessário.

Solicitei o número 25356 para ilustrar as soluções possíveis e suas desvantagens.

De @stevengj 's exemplo ["bug", "cow", "house"] .* "s" ----> ["bugs", "cows", "houses"] , iteratividade não parece ser suficiente, uma vez que as cordas são iterable mas agir como escalares lá. Se você precisar definir uma característica de qualquer maneira, pode ser melhor continuar exigindo a aceitação para transmissão, em vez de adicionar requisitos a todos os iteradores.

Felizmente keys(dict) agora retorna um AbstractSet , portanto, se adicionássemos um traço de transmissão para AbstractSet , o exemplo seria corrigido no OP. Também poderíamos adicionar um erro para transmitir Generator para capturar alguns casos comuns.

Transmitir por meio de contêineres AbstractSet parece inerentemente um pouco problemático: você pode combinar um AbstractSet com um escalar, mas não com qualquer outro contêiner, pois a ordem de iteração não é especificada para um conjunto. Isso quebra o significado usual de uma operação de "transmissão".

Sim, percebi ao preparar o PR que os conjuntos não são realmente o melhor exemplo de iteradores que deveriam suportar transmissão. Coisas como Generator e ProductIterator são casos muito mais interessantes.

Talvez a resposta seja (tentar) transmitir iteradores que têm HasShape e continuar a tratar todo o resto como escalares? Não vai consertar o OP, mas é bem elegante de outra forma.

Outro pensamento aleatório: talvez a transmissão sobre 1 argumento (como em string.(x) ) deva ser um caso especial que funcione mais como map , já que a compatibilidade de formatos não é um problema?

Talvez a resposta seja (tentar) transmitir iteradores que tenham HasShape e continuar a tratar todo o resto como escalares? Não vai consertar o OP, mas é bem elegante de outra forma.

Não tenho certeza se temos um forte motivo para excluir HasLength iteradores. Oferecemos suporte à transmissão por tuplas (que não implementam size ), então por que não tratar iteradores sem forma como tuplas? Por exemplo, faria todo o sentido poder usar o resultado de keys(::OrderedDict) com broadcast . Se não o apoiarmos, as pessoas ficarão tentadas a definir seus iteradores como HasShape apenas para serem utilizáveis ​​com broadcast (e a bela sintaxe de pontos).

Para citar Steve,

broadcast é para recipientes com formas

HasShape parece ser uma maneira razoável de definir isso com mais precisão. Caso contrário, parece-me que precisaríamos quebrar o comportamento de broadcast em strings, por exemplo.

Já temos uma inconsistência, com tuplas sendo consideradas contêineres e strings como escalares. Strings são muito especiais de qualquer maneira, não acho que seu comportamento se deva ao fato de não possuírem forma: está mais relacionado ao fato de serem a única coleção que é mais frequentemente considerada um escalar do que um contêiner.

Talvez @stevengj possa desenvolver por que ele acha que broadcast só deveria suportar contêineres com uma forma? Você apoiaria a consideração de tuplas como escalares também?

Acho que a justificativa para tratar tuplas como contêineres em broadcast (# 16986) era que, na prática, elas costumavam ser usadas como vetores essencialmente estáticos, e tratá-las como "escalares" em broadcast simplesmente não era muito útil de qualquer maneira. Em contraste, as strings (a) são freqüentemente tratadas como "átomos" para operações de processamento de strings e (b) não têm indexação consecutiva em geral, então elas se encaixam muito mal na estrutura broadcast .

Em princípio, eu apoiaria HasShape iteradores sendo usados ​​como contêineres em broadcast . O principal problema, como observei acima, é que ter HasShape não garante que getindex funcione.

O principal problema, como observei acima, é que ter HasShape não garante que getindex funcione

Algo como # 22489 ajudaria, ou seja, ter um traço de iterador que indica se um iterador é indexável?

Algo como # 22489 ajudaria, ou seja, ter um traço de iterador que indica se um iterador é indexável?

Mas então apenas iteradores indexáveis ​​seriam suportados com broadcast ? Isso parece muito restritivo, pois seria muito útil poder fazer coisas como string.(itr, "1") para qualquer iterável (por exemplo, o resultado de keys(::OrderedDict) ), e a indexação não é necessária para implementá-lo. Acho melhor lançarmos um erro para todos os iteradores que não oferecem suporte à indexação em 0.7 / 1.0 e tentar oferecer suporte em versões subsequentes. De qualquer forma, não é muito útil tratar iteradores como escalares. Então, podemos implementar qualquer comportamento que quisermos nas versões 1.x.

@stevengj Concordo com seus argumentos sobre strings e tuplas, mas por que não deveríamos tratar HasLength iteradores como tuplas? Eu não li uma justificativa para isso até agora.

@nalimilan , tendo a pensar que apenas os iteradores indexáveis ​​+ hasshape devem ser suportados por broadcast . Tentar enfiar iteradores gerais nesta função confunde muito seu significado - em algum ponto, você deve apenas usar map .

seria muito útil poder fazer coisas string.(itr, "1") para qualquer iterável ... De qualquer forma, não é muito útil tratar iteradores como escalares.

O caso de strings contradiz isso - o próprio argumento "1" é iterável em seu exemplo. Toneladas de coisas são iteráveis ​​(por exemplo, PyObject s em PyCall definem start etc.), incluindo coisas como conjuntos não ordenados onde o conceito broadcast está realmente quebrado.

Observe também que # 24990 tornará map ainda mais fácil do que agora, por exemplo, você poderá fazer map(string(_,"1"), itr) .

@nalimilan , tendo a pensar que apenas iteradores indexáveis ​​+ hasshape devem ser suportados por broadcast. Tentar empinar iteradores gerais nesta função confunde muito seu significado - em algum ponto, você deve apenas usar map.

Não temos uma característica atualmente para iteradores indexáveis. Como você sugere entregar isso? Meu WIP PR # 25356 geraria um erro para iteradores que não suportam indexação, o que não parece tão ruim, supondo que não seja muito útil tratar iteradores como escalares. Se quisermos tratá-los como escalares, precisaremos de outra característica, certo?

Estou inclinado a levantar erros para todos os casos que não são completamente óbvios, para que possamos implementar qualquer comportamento no futuro, em vez de nos bloquearmos em um comportamento padrão que não é necessariamente muito útil (ou seja, tratar alguns iteradores como escalares) . Como mostra esse problema, o comportamento de broadcast leva tempo para ser projetado corretamente.

(FWIW, PyObject não soa como um grande exemplo para mim, pois IIUC implementa o protocolo de iteração apenas porque não sabe com antecedência se envolverá um iterador Python ou não. PyObject é claramente uma exceção aqui, assim como precisa usar a sobrecarga de getfield para aparecer como um objeto Julia padrão. Os conjuntos são um exemplo mais Juliano.)

Poderíamos adicionar uma característica para HasShape iteradores indexáveis, como foi sugerido em outro lugar.

Triage gosta da ideia de fazer a transmissão iterar sobre todos os argumentos (como mapa) e adicionar um caractere de operador (como fazer const & = Ref como proposto anteriormente em outra edição, ou talvez ~ ) para marcar explicitamente Argumentos 0-d.

@vtjnash , o que isso significa para um iterador não HasShape ? Você quer dizer que deseja transmitir para iterar em coisas como strings e conjuntos? A implementação atual de broadcast está intimamente ligada a getindex ... você já pensou em como a implementaria sem getindex , particularmente para combinar argumentos de dimensionalidade diferente?

Em teoria, deveria ser possível suportar iteradores não indexáveis ​​(pelo menos aqueles que têm uma ordenação significativa). Isso é fácil quando todas as entradas têm o mesmo formato; quando eles têm formas diferentes e o iterador tem uma forma diferente (menor) do que o resultado, algum armazenamento intermediário seria necessário.

Parece que o traço IteratorAccess de PR https://github.com/JuliaLang/julia/pull/22489 pode ser adaptado / reutilizado para detectar iteradores indexáveis. Saber quais iteradores são indexáveis ​​(e, portanto, devem implementar keys ) também é necessário para https://github.com/JuliaLang/julia/pull/24774.

Cc: @rfourquet

👍 A triagem recomenda que isso seja um erro (a escolha segura) ou ligue a cobrar (se for fácil de fazer).

A triagem poderia decidir sobre uma estratégia específica a ser adotada aqui? Por exemplo, o que é "isso" no comentário de @JeffBezanson acima? Devemos lançar erros para todos os iteradores que não suportam indexação (escolha mais segura por agora, para que possamos fazer o que quisermos mais tarde), ou devemos tratar alguns iteradores como escalares? Devemos adicionar uma característica para iteradores indexáveis ​​e, em caso afirmativo, sob que forma (nova característica vs. nova escolha para Base.IteratorSize )? Devemos adicionar uma característica para iteradores em geral (para que possamos distingui-los dos escalares)?

O seguinte comportamento parece bom:

  • Por padrão, tente iterar e transmitir todos os argumentos.
  • Dê um erro se isso não funcionar por qualquer motivo.
  • Passe Ref(x) ou [x] para forçar x a ser tratado como um escalar.
  • Adicione uma característica que pode ser definida para permitir que um novo tipo seja tratado como escalar em vez de fornecer um erro. Observe que isso não deve ser usado para escolher entre iterar e não iterar. É apenas para transformar o erro em comportamento escalar.

Você poderia esclarecer a nota sobre o último ponto (talvez com um exemplo)? Não tenho certeza do que significa para o traço existir, mas não ser usado para escolher entre iteração e não iteração.

Então, basicamente "tentar iterar e transmitir todos os argumentos" implica que precisamos definir BroadcastStyle para retornar Scalar() para todos os tipos que não sejam de coleção (notavelmente Number , Symbol e AbstractString )? Isso soa como o "traço" que o último item menciona.

Honestamente, eu acharia menos caro definir uma característica para iteráveis ​​do que definir uma característica para não interáveis ​​/ escalares. Receio que todos os tipos que não sejam de coleção em algum ponto implementem esse traço Scalar , porque isso pode ser útil em alguns casos (possivelmente raros).

Isso soa como o "traço" que o último marcador menciona

Não, o último marcador significa que se algo implementa iteração, então o broadcast a itera - o traço escalar terá desaparecido. Para alguns tipos comuns distintamente não iteráveis ​​(como subtipos de Type e Function ), então podemos querer ter um traço NotIterable que transforma o MethodError em uma iteração que produz um valor (aquele objeto). Na verdade, não me lembro por que isso foi necessário.

Então, basicamente, "tentar iterar e transmitir todos os argumentos" implica que precisamos definir BroadcastStyle para retornar Scalar () para todos os tipos de não coleção (notavelmente Number, Symbol e AbstractString)? Isso soa como o "traço" que o último item menciona.

Não, todos os subtipos escalares de Number iteram e, portanto, estão bem. Precisamos defini-lo como símbolo. AbstractString funcionaria como uma coleção.

Não gosto de qualquer projeto que exija que definamos um método para um tipo a ser tratado como escalar. Esse deve ser o padrão. Eu também não acho que as strings devem ser tratadas como contêineres para transmissão.

Ainda acho que a transmissão deve tratar apenas os iteradores HasShape como contêineres; isso é consistente com o design da transmissão desde o início. O que há de errado com isso?

O problema com isso é o do OP; se você tiver um iterador sem forma, tratá-lo como um escalar dá uma resposta maluca.

Além disso, eu ficaria perfeitamente feliz em abandonar a parte "característica" da proposta. Ninguem reclama sobre

julia> map(string, [1,2], :a)
ERROR: MethodError: no method matching start(::Symbol)

Indiscutivelmente, a razão pela qual o resultado no OP é inesperado é que ninguém realmente pretende que uma chamada de broadcast trate _todos_ os argumentos como escalares; se houver apenas um argumento e houver alguma maneira de tratá-lo como uma coleção / iterador, é quase certo que o usuário pretende. Embora, é claro, 1 .+ 1 deva continuar a funcionar?

Isso me ocorreu, mas parece confuso fazer de um argumento um caso especial.

Vejo a seguinte assimetria: tratar um iterável como escalar dá resultados realmente estranhos, mas tratar um escalar como iterável dá um erro. Quando você obtém o erro, é fácil de corrigir envolvendo o argumento. Enquanto no primeiro caso não há nada simples que você possa fazer para fazê-lo iterar sobre o argumento.

Ainda acho que a transmissão deve tratar apenas os iteradores HasShape como contêineres; isso é consistente com o design da transmissão desde o início. O que há de errado com isso?

@stevengj O que há de errado no IMHO é que ele faz com que algumas operações não funcionem quando um comportamento perfeitamente razoável poderia ser implementado: trate HasLength iteradores apenas como Tuple , que atualmente é um caso especial. Mesmo que não os apoiemos agora, gostaria de deixar em aberto a possibilidade de apoiá-los em algum ponto do 1.x.

Ninguem reclama sobre

julia> map (string, [1,2],: a)
ERROR: MethodError: nenhum método corresponde a start (:: Symbol)

@JeffBezanson OTC, apoio o comportamento atual de broadcast , que repete :a conforme necessário. Esse tipo de coisa pode ser muito útil, por exemplo, para renomear uma série de DataFrame colunas. Você sugere alterar broadcast para gerar um erro como map ?

Vejo a seguinte assimetria: tratar um iterável como escalar dá resultados realmente estranhos, mas tratar um escalar como iterável dá um erro. Quando você obtém o erro, é fácil de corrigir envolvendo o argumento. Enquanto no primeiro caso não há nada simples que você possa fazer para fazê-lo iterar sobre o argumento.

É fácil, mas bastante inconveniente. Concordo com @stevengj que os escalares devem ser transmitidos por padrão, não gerar um erro. Obviamente, Number tipos Symbol , não seria muito útil em geral. Char seria outro, e muitos tipos personalizados definidos no pacote também sofrerão com isso (e acabarão definindo seus BroadcastStyle como Scalar() ).

Acho que o ponto crucial da questão é que não temos uma característica para distinguir coleções de escalares. Portanto, a solução mais direta foi tratar HasShape iteradores como coleções e outros tipos como escalares (incluindo HasLength iteradores, já que é o padrão para todos os tipos). Pessoalmente, acho que introduzir um traço para coleções / iteráveis ​​faria muito sentido, mas se não estivermos prontos para fazer isso e se não pudermos contar com start sendo definido para detectar iteráveis, estou medo de ter que manter o comportamento atual.

A proposta de Jeff em https://github.com/JuliaLang/julia/issues/18618#issuecomment -360594955 tem espaço para permitir a transmissão tratando Symbol e Char como tipos "escalares" - eles apenas precisa aceitar o comportamento. Por padrão, eles seriam um erro, pois não implementam start .

A parte mais convincente aqui é que a única definição sensata para um tipo de "coleção" é que ele seja iterável. Sim, isso significa que as strings são coleções. Às vezes, eles são usados ​​como tal! Portanto, vamos adotar o comportamento que permite facilmente que as pessoas optem pelo outro no site da chamada.

No entanto, há uma verruga aqui. Como os números são iteráveis ​​(eles até HasShape ), eles serão tratados como contêineres de dimensão zero. Isso significa que, levado à sua conclusão lógica, 1 .+ 2 != 3 . Em vez disso, seria fill(3, ()) .

EDITAR: em uma tentativa de evitar atrapalhar a discussão, mudou-se para o discurso:

https://discourse.julialang.org/t/lazycall-again-sorry/8629

A proposta de Jeff em # 18618 (comentário) tem espaço para permitir a transmissão tratando Symbol e Char como tipos "escalares" - eles só precisam aceitar o comportamento. Por padrão, eles seriam um erro, pois não implementam iniciar.

Sim, minha posição se baseia apenas na suposição de que os escalares são um fallback mais natural, especialmente considerando que as coleções precisam implementar alguns métodos (iteração, possivelmente indexação), enquanto os escalares são apenas "o resto" e não têm nada em comum. No final, qualquer tipo será capaz de implementar qualquer comportamento que desejar, mas devemos torná-lo o mais conveniente e lógico possível, o que em particular deve ajudar a evitar inconsistências (por exemplo, alguns tipos declarados e comportando-se como escalares e outros não).

Não estou muito preocupado em ter algumas exceções para tipos essenciais como strings e números, desde que as regras sejam claras para outros tipos.

Estive pensando um pouco sobre nossas interfaces de iteração e indexação. Observo que podemos ter objetos úteis que iteram (mas não podem ser indexados), objetos que podem ser indexados (mas não são iteráveis) e objetos que fazem as duas coisas. Com base nisso, eu me pergunto se:

  • map pode estar fortemente ligado ao protocolo de iteração - parece válido que possamos fazer um out = map(f, iterable) preguiçoso para qualquer iterable arbitrário de forma que, por exemplo, first(out) seja o igual a f(first(iterable)) , e me parece que essa operação preguiçosa genérica pode ser útil.
  • broadcast pode estar fortemente ligado à interface de indexação - parece válido que possamos fazer um out = broadcast(f, indexable) preguiçoso de forma que out[i] seja o mesmo que f(indexable[i]) , e parece-me que essa operação preguiçosa genérica pode ser útil. Obviamente, broadcast com entradas múltiplas ainda pode fazer todas as coisas sofisticadas que faz agora. Para fins de transmissão, escalares seriam aquelas coisas que não podem ser indexadas (ou indexar trivialmente como Number e Ref e AbstractArray{0} ).

Eu também acho que seria desejável se um argumento map e um argumento broadcast fizessem coisas muito semelhantes para coleções que são iteráveis ​​e indexáveis. No entanto, o fato de que AbstractDict iteration retorna coisas diferentes de getindex parece bloquear uma boa unificação aqui. : frowning_face: (Nossos outros tipos de coleção parecem bons)

(Para mim, o fato de que coisas como strings podem ter que ser explicitamente embrulhadas como ["bug", "cow", "house"] .* ("s",) não soa como um quebra-negócio aqui. Tenho o mesmo problema quando quero pensar em um vetor de 3 como sendo um "único ponto 3D" e não é muito difícil de lidar (refª # 18379)).

Concordo que broadcast deve ser para contêineres indexáveis, mas acho que deve ser indexável consecutivamente , o que exclui strings. por exemplo, collect(eachindex("aαb🐨γz"))[1, 2, 4, 5, 9, 11] , que funcionará mal com qualquer implementação de broadcast baseada na indexação.

Mas ser para contêineres indexáveis ​​significa essencialmente que os contêineres precisam de uma característica de ativação, que é basicamente o que venho defendendo.

Não tenho certeza se índices consecutivos são uma boa restrição - dicionários terão índices arbitrários, por exemplo.

No entanto, broadcast(f, ::String) não pode criar um novo String e garantir que os índices de saída permaneçam iguais aos índices de entrada, uma vez que as larguras dos caracteres UTF-8 podem mudar em f ( teria que se transformar em algo como AbstractDict{Int, Char} para fazer essa garantia, o que realmente não parece muito útil!). Eu quase diria que os índices de String são mais como "tokens" para pesquisa rápida do que índices semanticamente importantes (por exemplo, você pode converter para uma string UTF-32 equivalente e os índices mudariam).

Não me importo se fizermos o opt-in do comportamento de transmissão via trait; Só estou dizendo que imaginar como um broadcast(f, ::Any) genérico se comporta é uma boa maneira de orientar a implementação de coisas como broadcast(f, ::AbstractDict) (e naturalmente responderia à pergunta que levantei em # 25904, ou seja, transmitido valores de dicionário e não pares de valores-chave).

As pessoas estão realmente felizes com essa mudança? Eu, pelo menos, nunca precisei transmitir por meio de um contêiner sem forma, enquanto transmito por coisas que deveriam ser tratadas como escalares o tempo todo . Cada aviso de depreciação que eu 'conserto' me faz derramar uma lágrima.

Eu transmito coisas que devem ser tratadas como escalares _ todo o tempo_.

Quais são os tipos dessas coisas?

Pode ser qualquer coisa. Por exemplo, em um pacote que define um tipo de modelo de otimização Model e um tipo de variável de decisão Variable , você pode ter x::Vector{Variable} para o qual deseja obter os valores após resolver o modelar model usando uma função value(::Variable, ::Model)::Float64 . Anteriormente, você podia fazer isso como:

value.(x, model)

Também é comum que os tipos de argumento sejam de outros pacotes, portanto, adicionar um método a broadcastable para esses tipos seria pirataria de tipo nesse caso. Portanto, você deve usar Ref ou uma tupla de um elemento. Isso não é intransponível, mas apenas torna o caso comum muito menos elegante para oferecer suporte a um padrão de uso relativamente obscuro, na minha opinião.

Sim, entendo o que você quer dizer e concordo que é irritante em situações como essa. Dito isso, o comportamento antigo era absolutamente problemático - era uma daquelas coisas "o fallback padrão é definitivamente errado em alguns casos".

Resumindo, existem quatro opções que evitam o fallback incorreto:

  1. requerem _tudo_ para implementar algum método que descreva como eles transmitem
  2. Padrão para tratar as coisas como contêineres e erro / depreciar para não contêineres.

    • Tentaremos apenas iterate objetos desconhecidos e isso causará um erro para escalares

    • Existem duas saídas de emergência para escalares - os usuários podem envolvê-los em um site de chamada e os autores da biblioteca podem optar por uma transmissão semelhante a escalar não embalada.

  3. Padrão para tratar coisas como escalares e erro para contêineres desconhecidos

    • Dado que não há métodos relevantes definidos apenas para escalares, teríamos que afirmar que iterate gera um erro de método. Isso é lento e tortuoso.

    • Haveria apenas uma saída de emergência disponível para que os containers personalizados não cometessem erros: os autores de suas bibliotecas optassem explicitamente pela transmissão. Isso parece um tanto retrógrado para uma função cujo objetivo principal é trabalhar com contêineres.

  4. Verifique applicable(iterate, …) e mude os comportamentos de acordo

    • Isso atualmente não funciona devido ao mecanismo de depreciação de start / next / done e, em geral, pode estar errado para tipos de wrapper que adiam métodos para um membro.

A opção 1 é pior para todos, a opção 2 é o status quo e a opção 3 é para trás e a opção 4 é algo que nunca fizemos antes e provavelmente está cheio de bugs.

Acho que parte da discussão deve ter acontecido nos bastidores, mas não estou convencido pelos argumentos que vi neste tópico e em https://github.com/JuliaLang/julia/pull/25356 contra nalimilan 'se posições de stevengj .

Haveria apenas uma saída de emergência disponível para que os containers personalizados não cometessem erros: os autores de suas bibliotecas optassem explicitamente pela transmissão. Isso parece um tanto retrógrado para uma função cujo objetivo principal é trabalhar com contêineres.

Este é o meu principal ponto de desacordo. Para mim, parece que em todo o código Julia # of iterator types << # of types that should be treated as scalars in a broadcast situation < # of broadcast calls . Portanto, prefiro que o número de vezes que algo 'extra' precise ser feito seja escalonado com o número de tipos de iteradores, em vez de com o número de chamadas de broadcast. E se um autor de biblioteca define um iterador, não é completamente irracional pedir-lhe para definir mais um método, ao passo que _é_ completamente irracional pedir a cada autor de pacote para definir Base.broadcastable(x) = Ref(x) para todos os seus tipos não iteráveis, a fim de evite (IMHO) Ref s feios em uma alta porcentagem de broadcast ligações.

Eu sei que ter um único método para implementar a definir iteração é bom, mas não é que muito trabalho para implementar mais um tanto para uma nova característica, ou para torná-lo necessário especificar Base.iteratorsize para uma nova iteração (e livrando-se do problemático HasLength default). O método de fallback broadcastable poderia então ser baseado nessa característica. Ou, se você realmente adora definir iteração com um único método, pode (pós-deprecação-remoção) fazer com que esse padrão explícito seja applicable(iterate, ...) como em https://github.com/JuliaLang/ julia / issues / 18618 # issuecomment -354618742 e simplesmente sobrescrever esse padrão se necessário. Casos esquivos como String também podem ser tratados com a especialização adicional de broadcastable se desejado.

Esse é efetivamente o design de 0,6, que levou a este problema e # 26421 e # 19577 e # 23197 e # 23746 e possivelmente mais - procurar por isso é difícil.

Isso significa que o Base está fornecendo um fallback padrão incorreto para uma classe inteira de objetos. É por isso que prefiro um mecanismo com erros, a menos que você aceite, de uma forma ou de outra. É opinativo e a transição é uma dor, mas força você a ser explícito.

Você pode estar certo ao dizer que existem mais tipos personalizados "semelhantes a escalares" do que semelhantes a iteradores, mas mantenho o fato de que a transmissão é, antes de mais nada, uma operação em contêineres. Espero que f.(x) faça algum tipo de mapeamento e não seja apenas f(x) .

E, finalmente, os contêineres que recebem o tratamento padrão escalar simplesmente não podem ser usados ​​elemento a elemento com a transmissão. Por exemplo, String é um tipo de coleção que criamos uma caixa especial para se comportar como um escalar; não é possível "alcançar" e trabalhar elemento a elemento, embora isso pareça fazer sentido em algumas situações (por exemplo, isletter.("a1b2c3") ). Esse é o argumento da assimetria: você pode empacotar contêineres com mais eficiência em um Ref para tratá-los como escalares do que collect em uma coleção realmente transmitível.

Esses são os principais argumentos. No que diz respeito à feiúra de Ref, concordo plenamente. Uma solução é # 27608.

É justo. Não tenho nenhum argumento decisivo ou soluções mágicas para esses problemas e https://github.com/JuliaLang/julia/pull/27608 vai melhorar as coisas.

@tkoolen Eu tive as mesmas preocupações e caso de uso .

@mbauman Os argumentos dados acima podem não ser totalmente convincentes. Aqui estão duas perguntas para ser mais completo:

1) Seria possível fazer de broadcastable uma interface necessária para qualquer iterável.
Isso seria completamente sistemático e forçaria os desenvolvedores a pensar sobre
como seu iterador deve se comportar durante a transmissão.
Uma recomendação para defini-lo como collect(x) tornaria a transição relativamente fácil na maioria dos casos.
Não haveria nenhuma perda de desempenho, certo?

2) Portanto, tudo se resume à vontade de ter um erro para f.(x) se x difundir como um escalar.
Por que não um aviso / erro do linter para f.(x, y, z) , como "todos os argumentos de 'f' transmitidos como escalares"?

De qualquer forma, pode ser sábio corrigir o # 27563 (por exemplo, # 27608) e permitir que os usuários brinquem com ele um pouco antes de 1.0.
[0.7 e 1.0.0-rc1.0 foram lançados sem uma correção].

De qualquer forma, pode ser sábio corrigir o # 27563 (por exemplo, # 27608) e permitir que os usuários brinquem com ele um pouco antes de 1.0.
[0.7 e 1.0.0-rc1.0 foram lançados sem uma correção].

Suponho que você perdeu a notícia de que o 1.0 foi lançado .

@StefanKarpinski Senti falta disso, de fato. Parabéns a todos os desenvolvedores, julia é incrível, continue!

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

Questões relacionadas

m-j-w picture m-j-w  ·  3Comentários

helgee picture helgee  ·  3Comentários

StefanKarpinski picture StefanKarpinski  ·  3Comentários

sbromberger picture sbromberger  ·  3Comentários

Keno picture Keno  ·  3Comentários