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)
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:
Ref(x)
ou [x]
para forçar x
a ser tratado como um 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:
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"))
dá [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:
iterate
objetos desconhecidos e isso causará um erro para escalaresiterate
gera um erro de método. Isso é lento e tortuoso.applicable(iterate, …)
e mude os comportamentos de acordoA 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!
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", enquantomap
itera sobre os caracteres.