Julia: 广播只有一项工作(例如通过迭代器和生成器广播)

创建于 2016-09-21  ·  69评论  ·  资料来源: JuliaLang/julia

令人惊讶地发现broadcast不适用于迭代器

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

这是由于Broadcast.containertype返回Any https://github.com/JuliaLang/julia/blob/413ed79ec54f3a754ac8bc57c1d29835d17bd274/base/broadcast.jl#L31
导致回退: https :

containertype定义为该迭代器的Array会导致在其上调用size时出现问题,因为broadcast不检查迭代器接口iteratorsize(IterType)

map用回退map(f, A) = collect(Generator(f,A)) map解决了这个问题,这可能比broadcast(f, Any, A) = f(A)的当前定义更合理

broadcast

最有用的评论

这是故意的。 broadcast用于具有形状的容器,默认将对象视为标量。 map用于没有形状的容器,默认将对象视为迭代器。

例如, broadcast将字符串视为“标量”,而map遍历字符。

所有69条评论

这是故意的。 broadcast用于具有形状的容器,默认将对象视为标量。 map用于没有形状的容器,默认将对象视为迭代器。

例如, broadcast将字符串视为“标量”,而map遍历字符。

也许问题是人们发现新的点语法太方便了。 过去一直希望有一种紧凑的方式来表达map 。 不幸的是,点语法已经被采用了。

此外,正如@stevengj之前指出的那样: mapbroadcast之间必须存在差异,如果没有,那么两者都有什么意义。

@stevengj但迭代器确实有形状(尤其是生成器) http://docs.julialang.org/en/release-0.5/manual/interfaces/#interfaces

我认为迭代器在这个尴尬的领域是你想用容器做的大多数事情,你也想用迭代器做,是的,也许这纯粹是因为.语法太方便了(并且您得到的错误非常不透明)。

@pabloferz mapbroadcast之间的主要区别在于标量的处理。 现在标量的定义是有争议的,我想说所有具有length(x) > 1都不应该被视为标量。

标记哪些参数将被视为可迭代,而不是函数调用本身,将消除歧义。 我认为?

对于broadcast (我也相信一般)具有形状,意味着具有size (不仅仅是length )并且是可索引的。 除了元组之外,没有size任何其他东西都被视为标量。 鉴于当前的实现,您首先需要getindex或能够为要广播的对象定义一个。 对于迭代器,这通常是不可能的。

我也遇到了这个。 来自#16769,我在那里寻找一种方法来fill!一个具有重复评估函数(而不是固定值)的数组,我认为点语法可能已经可以解决问题。 但是,当a = zeros(2, 3); a .= [rand() for i=1:2, j=1:3]起作用时,(会)更便宜的a .= (rand() for i=1:2, j=1:3)不起作用; 这个生成器是HasShape() ,但确实没有索引功能。 我对广播/点语法的工作原理非常了解,但是在这里拥有索引功能的特征会有所帮助吗? 已经有一个 PR (#22489) 了...

@rfourquet ,你可以做a = zeros(2, 3); a .= rand.()

是的,但我应该更精确:我想使用一个函数来获取索引作为参数,比如a .= (f(i, j) for i=1:2, j=1:3)

HasShape迭代器广播维度的缺点是什么? 这听起来很自然。

@nalimilan ,乍一看我认为这是合理的,并且可能相对容易实施。 会打破所以应该由1.0完成。

一个潜在的问题是HasShape迭代器不一定支持getindex ,这可能会使实现变得棘手?

一种可能性是暂时(对于 1.0)进行简单的实现,只是复制到数组中。 这将允许在 1.0 后进行优化

一个潜在的问题是 HasShape 迭代器不一定支持 getindex,这可能会使实现变得棘手?

正如我上面所说的,我在 #22489 有一个 PR 允许索引到迭代器,如果这有帮助的话。

需要为 1.0 做些什么才能至少改进 1.x 中的行为?

感谢@nalimilan提出这个问题,我也想这样做。 如果在 1.0 中无法实现广播表达式右侧的HasShape生成器,我们现在是否应该将其设为错误,而不是将生成器视为标量? 这样就可以在 1.x 中启用。

:+1: Triage 建议将其设为错误(安全选择)或对其调用collect (如果容易的话)。

map将其所有参数视为容器,并尝试遍历所有参数。 在我的理想世界中, broadcast将是相似的,并且将其所有参数视为具有可以广播的形状,如果未定义例如size则给出错误。 我会指出,任何值都可以通过用fill将其包装在广播中被视为标量,从而产生一个 0-d 数组:

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

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

您真的建议默认将所有标量视为容器吗? 这听起来不太实用。

看看我们如何支持任何可迭代对象,或者只是在我们支持它们之前为它们抛出错误,看起来我们需要一种方法来识别BroadcastStyle迭代器。 目前,这是不可能的,因为Base.iteratorsize返回HasLength即使像标量Symbol 。 我们可以引入一个Base.isiterable trait(这可能对其他事情有用),或者让Base.iteratorsize默认为NotIterable (这也有意义,因为HasLength作为默认值听起来总是有点令人惊讶,如果无害的话)。

(未来讨论的棘手案例: UniformScaling 。)

@timholy既然你已经完成了broadcast的重新设计,有什么建议吗?

@JeffBezansonbroadcast的全部意义在于能够“广播”标量以匹配容器,例如执行["bug", "cow", "house"] .* "s" ----> ["bugs", "cows", "houses"] 。 这与map的行为根本不同。

这就是broadcast默认将对象视为标量的原因,以便它们可以与容器组合。 为无法识别的类型抛出错误会使其变得不那么有用。

应该可以通过在其上定义一些适当的方法来将特定类型声明为broadcast的容器,但我认为默认应该继续将对象视为标量。

在一个不相关的 PR (https://github.com/JuliaLang/julia/pull/25339) 中, @Keno建议使用applicable(start, (x,))来确定x是否可迭代。 我们应该在这里使用相同的方法吗? 我会发现对迭代器有更明确的定义(基于Base.iteratorsize或特征)会更清楚,但使用start也有点道理。

我们可以有一个默认为applicable(start, (x,))的显式特征; 这将允许在必要时覆盖它。

我已提交 #25356 来说明可能的解决方案及其缺点。

@stevengj的例子["bug", "cow", "house"] .* "s" ----> ["bugs", "cows", "houses"]来看,可迭代性似乎还不够,因为字符串是可迭代的,但在那里就像标量一样。 如果您无论如何都需要定义特征,最好继续要求选择加入广播,而不是向所有迭代器添加要求。

幸运的是keys(dict)现在返回一个AbstractSet ,因此如果我们为AbstractSet添加广播特征,它将修复 OP 中的示例。 我们还可以添加一个错误来广播Generator以捕获一些常见情况。

AbstractSet容器上广播似乎本质上有点问题:您可以将AbstractSet与标量结合使用,但不能与任何其他容器结合使用,因为未指定集合的​​迭代顺序。 这种打破了“广播”操作的通常含义。

是的,我在准备 PR 时意识到,集合确实不是应该支持广播的迭代器的最佳示例。 诸如GeneratorProductIterator类的事情要有趣得多。

也许答案是(尝试)广播具有HasShape迭代器,并继续将其他所有内容视为标量? 不会修复 OP,但在其他方面非常优雅。

其他随机想法:也许广播超过 1 个参数(如string.(x) )应该是一个更像map的特殊情况,因为形状兼容性不是问题?

也许答案是(尝试)广播具有 HasShape 的迭代器,并继续将其他所有内容视为标量? 不会修复 OP,但在其他方面非常优雅。

我不确定我们是否有充分的理由排除HasLength迭代器。 我们支持在元组上广播(它没有实现size ),那么为什么不把不成形的迭代器当作元组来对待呢? 例如,能够将keys(::OrderedDict)broadcast一起使用是非常有意义的。 如果我们不支持它,人们会很想将他们的迭代器定义为HasShape只是为了与broadcast (以及漂亮的点语法)。

引用史蒂夫的话,

broadcast用于具有形状的容器

HasShape似乎是更精确地定义它的合理方法。 否则,在我看来,例如,我们需要打破字符串广播的行为。

我们已经有了不一致之处,元组被视为容器,字符串被视为标量。 无论如何,字符串非常特殊,我不认为它们的行为是由于它们没有形状:这与它们是唯一一个通常被视为标量而不是容器的集合有关。

也许@stevengj可以解释为什么他认为broadcast应该只支持具有形状的容器? 您是否也支持将元组视为标量?

我认为在broadcast (#16986) 中将元组视为容器的基本原理是,在实践中它们通常被用作本质上是静态的向量,而在broadcast中将它们视为“标量”并不是反正很有用。 相比之下,字符串 (a) 通常被视为字符串处理操作的“原子”,并且 (b) 通常没有连续索引,因此它们非常不适合broadcast框架。

原则上,我会支持将HasShape迭代器用作broadcast容器。 正如我上面提到的,主要问题是拥有HasShape并不能保证getindex有效。

正如我上面提到的,主要问题是拥有 HasShape 并不能保证 getindex 工作

像#22489 这样的东西会有所帮助,即具有指示迭代器是否可索引的迭代器特征?

像#22489 这样的东西会有所帮助,即具有指示迭代器是否可索引的迭代器特征?

但是只有broadcast才支持可索引的迭代器? 这听起来太严格了,因为能够为任何可迭代对象(例如keys(::OrderedDict)的结果)执行诸如string.(itr, "1")类的操作非常有用,并且不需要索引来实现它。 我认为我们最好在 0.7/1.0 中对所有不支持索引的迭代器抛出一个错误,并尝试在后续版本中支持它们。 无论如何,将迭代器视为标量并不是很有用。 然后我们可以在 1.x 版本中实现我们想要的任何行为。

@stevengj我同意你关于字符串和元组的论点,但为什么我们不应该将HasLength迭代器视为元组? 直到现在我还没有读过这个理由。

@nalimilan ,我倾向于认为broadcast应该只支持 indexable+hasshape 迭代器。 试图将通用迭代器塞进这个函数会混淆它的含义——在某些时候,你应该只使用map

能够为任何可迭代对象执行string.(itr, "1")将非常有用……无论如何,将迭代器视为标量并不是很有用。

字符串的情况与此相矛盾 - "1"参数本身在您的示例中是可迭代的。 的事情是可迭代(例如PyObject S IN PyCall限定start等),包括像无序集合,其中broadcast概念是真正破裂。

另请注意,#24990 将使map比现在更容易,例如您将能够执行map(string(_,"1"), itr)

@nalimilan ,我倾向于认为广播应该只支持 indexable+hasshape 迭代器。 试图将通用迭代器塞进这个函数会混淆它的含义——在某些时候,你应该只使用 map。

我们目前没有可索引迭代器的特征。 你建议如何处理? 我的 WIP PR #25356 会为不支持索引的迭代器抛出一个错误,假设将迭代器视为标量并不是很有用,这听起来还不错。 如果我们想将它们视为标量,我们需要另一个特征,对吗?

我倾向于为所有不完全明显的情况提出错误,以便我们可以在将来实现任何行为,而不是将自己锁定在不一定非常有用的默认行为中(即将某些迭代器视为标量) . 正如这个问题所示, broadcast的行为需要时间来正确设计。

(FWIW, PyObject对我来说听起来不是一个很好的例子,因为 IIUC 它实现了迭代协议,只是因为它事先不知道它是否会包装 Python 迭代器。 PyObject显然是这里的一个例外,就像它需要使用getfield重载来看起来像一个标准的 Julia 对象。集合是一个更像朱利安的例子。)

我们可以为可索引的HasShape迭代器添加一个特征,正如其他地方所建议的那样。

Triage 喜欢让广播迭代所有参数(如 map),并添加一个操作符字符(如之前在另一个问题中提出的const & = Ref~ )来明确标记0-d 参数。

@vtjnash ,这对非HasShape迭代器意味着什么? 你的意思是你想要广播来迭代字符串和集合之类的东西吗? 当前的broadcast实现与getindex密切相关……你有没有想过如何在没有getindex情况下实现它,特别是对于组合不同维度的参数?

理论上应该可以支持不可索引的迭代器(至少那些具有有意义的排序)。 当所有输入具有相同的形状时,这很容易; 当它们具有不同的形状并且迭代器具有与结果不同(较小)的形状时,将需要一些中间存储。

看起来来自 PR https://github.com/JuliaLang/julia/pull/22489IteratorAccess trait 可以被改编/重用来检测可索引的迭代器。 https://github.com/JuliaLang/julia/pull/24774还需要知道哪些迭代器是可索引的(因此应该实现keys

抄送: @rfourquet

👍 Triage 建议将其设为错误(安全选择)或调用 collect(如果容易的话)。

分类可以决定在此处采用的特定策略吗? 例如, @JeffBezanson上面的评论中的“this”是什么? 我们应该为所有不支持索引的迭代器抛出错误(目前最安全的选择,以便我们以后可以做任何我们想做的事情),还是应该将一些迭代器视为标量? 我们是否应该为可索引迭代器添加一个特征,如果是,以什么形式(新特征与Base.IteratorSize新选择)? 我们是否应该为迭代器添加一个 trait(以便我们可以将它们与标量区分开来)?

以下行为似乎很好:

  • 默认情况下,尝试迭代和广播每个参数。
  • 如果无论出于何种原因都不起作用,请给出错误。
  • 通过Ref(x)[x]强制将x视为标量。
  • 添加可以定义为允许将新类型视为标量而不是给出错误的特征。 请注意,这不应用于在迭代与不迭代之间进行选择。 这只是将错误转化为标量行为。

你能澄清最后一点的注释吗(也许有一个例子)? 我不确定特征存在意味着什么,但不能用于在迭代与不迭代之间进行选择。

所以基本上“尝试迭代和广播每个参数”意味着我们需要定义BroadcastStyle来为所有非集合类型返回Scalar() (特别是NumberSymbolAbstractString )? 这听起来像最后一个项目符号提到的“特征”。

老实说,我发现为可迭代对象定义特征比为不可交互/标量定义特征成本更低。 我担心所有非集合类型都会在某个时候实现Scalar特性,因为这在某些(可能很少见)情况下很有用。

这听起来像是最后一项提到的“特质”

不,最后一个项目符号意味着如果某些东西实现了迭代,那么广播会对其进行迭代——Scalar trait 将消失。 对于一些常见的明显不可迭代的类型(例如TypeFunction ),那么我们可能希望有一个NotIterable trait 来改变MethodError进入产生一个值(该对象)的迭代。 我实际上不记得为什么这是必要的。

所以基本上“尝试迭代和广播每个参数”意味着我们需要定义 BroadcastStyle 来为所有非集合类型(特别是 Number、Symbol 和 AbstractString)返回 Scalar()? 这听起来像最后一个项目符号提到的“特征”。

不, Number所有标量子类型都会自我迭代,所以没问题。 我们需要为符号定义它。 AbstractString将作为集合运行。

我不喜欢任何要求我们为类型定义方法的设计,以将其视为标量。 那应该是默认的。 我也不认为字符串应该被视为广播的容器。

我仍然认为广播应该只将 HasShape 迭代器视为容器; 这与一开始的广播设计是一致的。 这有什么问题?

问题在于 OP 中的问题; 如果您有一个没有形状的迭代器,将其视为标量会给出一个疯狂的答案。

此外,我非常乐意放弃提案中的“特质”部分。 没有人抱怨

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

可以说 OP 中的结果出乎意料的原因是没有人真的打算通过广播调用将 _all_ 参数视为标量; 如果只有一个参数并且有任何方法可以将其视为几乎可以肯定用户想要的集合/迭代器。 虽然当然1 .+ 1应该继续工作?

我想到了这一点,但将一个论点视为特例似乎令人困惑。

我看到以下不对称:将可迭代对象视为标量会产生非常奇怪的结果,但将标量视为可迭代会产生错误。 当您收到错误时,通过包装参数很容易修复。 而在第一种情况下,没有什么简单的方法可以让它迭代参数。

我仍然认为广播应该只将 HasShape 迭代器视为容器; 这与一开始的广播设计是一致的。 这有什么问题?

@stevengj 有什么问题恕我直言,当可以实现完全合理的行为时,它会使某些操作不起作用:将HasLength迭代器视为Tuple ,目前是特殊情况。 即使我们现在不支持它们,我也愿意保留在 1.x 中某个时候支持它们的可能性。

没有人抱怨

茱莉亚> 地图(字符串,[1,2],:a)
错误:方法错误:没有方法匹配开始(::符号)

@JeffBezanson OTC,我支持broadcast的当前行为,它根据需要重复:a 。 这种事情非常有用,例如重命名一系列DataFrame列。 您是否建议将broadcast更改为引发map类的错误?

我看到以下不对称:将可迭代对象视为标量会产生非常奇怪的结果,但将标量视为可迭代会产生错误。 当您收到错误时,通过包装参数很容易修复。 而在第一种情况下,没有什么简单的方法可以让它迭代参数。

这很容易,但很不方便。 我同意@stevengj 的观点,即默认情况下应该广播标量,而不是引发错误。 当然,由于Number类型是可迭代的,因此烦恼并不总是可见的,但正如Symbol示例所示,它通常不会很有帮助。 Char将是另一个,并且在 package 中定义的许多自定义类型也会受到此影响(最终将它们的BroadcastStyleScalar() )。

我认为问题的关键是我们没有区分集合和标量的特征。 因此,最直接的解决方案确实是将HasShape迭代器视为集合,将其他类型视为标量(包括HasLength迭代器,因为这是所有类型的默认值)。 就我个人而言,我认为为集合/可迭代对象引入 trait 会很有意义,但如果我们还没有准备好这样做,并且如果我们不能依赖定义的start来检测可迭代对象,我会恐怕我们将不得不保持当前的行为。

杰夫在https://github.com/JuliaLang/julia/issues/18618#issuecomment -360594955 中的提议有空间允许广播将SymbolChar视为“标量”类型——他们只是需要选择加入该行为。 默认情况下,它们将是一个错误,因为它们没有实现start

这里最引人注目的部分是“集合”类型的唯一合理定义是它是可迭代的。 是的,这意味着字符串是集合。 有时它们被这样使用! 因此,让我们默认为易于人们在呼叫站点选择加入其他人的行为。

不过,这里有一个疣。 由于数字是可迭代的(它们甚至是HasShape ),它们将被视为零维容器。 这意味着,根据其逻辑结论, 1 .+ 2 != 3 。 而是fill(3, ())

编辑:为了避免进一步使线程脱轨,转移到话语中:

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

Jeff 在 #18618(评论)中的提议允许广播将 Symbol 和 Char 视为“标量”类型——他们只需要选择加入这种行为。 默认情况下,它们将是一个错误,因为它们没有实现 start。

是的,我的立场只是基于这样一个假设,即标量是更自然的后备,特别是考虑到集合需要实现一些方法(迭代,可能是索引)而标量只是“其余部分”并且没有任何共同点。 最后,任何类型都可以实现它想要的任何行为,但我们应该使其尽可能方便和合乎逻辑,这尤其有助于避免不一致(例如,某些类型声明并表现为标量,而另一些则不是)。

只要规则对于其他类型是明确的,我并不太关心对基本类型(如字符串和数字)有一些例外情况。

我一直在思考我们的迭代和索引接口。 我注意到我们可以有迭代(但不能被索引)的有用对象,可以被索引(但不可迭代)的对象,以及同时执行这两种操作的对象。 基于此,我想知道:

  • map可能与迭代协议紧密相关 - 我们可以为任意iterable制作一个懒惰的out = map(f, iterable)似乎是有效的,例如first(out)是与f(first(iterable)) ,在我看来,这种通用的惰性操作可能很有用。
  • broadcast可能与索引接口紧密相关 - 我们可以制作一个懒惰的out = broadcast(f, indexable)使得out[i]f(indexable[i])相同,这似乎是有效的,并且在我看来,这种通用的惰性操作可能很有用。 显然,带有多个输入的broadcast仍然可以做它现在所做的所有花哨的事情。 为了广播的目的,标量是那些不能被索引的东西(或者像NumberRefAbstractArray{0}那样简单的索引)。

我也觉得如果一个参数map和一个参数broadcast对可迭代和可索引的集合做非常相似的事情,那将是可取的。 然而, AbstractDict迭代返回的东西与getindex不同的事实似乎在这里阻碍了一个很好的统一。 :frowning_face:(我们的其他集合类型看起来不错)

(对我来说,像字符串这样的东西可能必须像["bug", "cow", "house"] .* ("s",)那样显式地包装起来,这在这里听起来不像是一个破坏交易的东西。当我想考虑一个 3 向量时,我也有同样的问题作为“单个 3D 点”,处理起来并不太难(外部参照 #18379))。

我同意broadcast应该用于可索引容器,但我认为它应该是连续可索引的,不包括字符串。 例如collect(eachindex("aαb🐨γz"))给出了[1, 2, 4, 5, 9, 11] ,这对于任何基于索引的broadcast实现都会很糟糕。

但是对于可索引容器来说,本质上是容器需要一个特性来选择加入,这基本上是我一直提倡的。

我不确定连续索引是否是一个很好的约束 - 例如,字典将具有任意索引。

但是, broadcast(f, ::String)不能创建新的String并保证输出索引与输入索引保持相同,因为 UTF-8 字符宽度可能会在f (它必须变成AbstractDict{Int, Char}类的东西才能做出保证,这看起来真的不是很有用!)。 我几乎会说String的索引更像是用于快速查找的“标记”而不是语义上重要的索引(例如,您可以转换为等效的 UTF-32 字符串并且索引会改变)。

我不介意我们是否通过 trait 选择加入广播行为; 我只是说想象一个通用的broadcast(f, ::Any)行为是指导broadcast(f, ::AbstractDict)类的东西的实现的好方法(并且自然会回答我在 #25904 中提出的问题,即广播字典值而不是键值对)。

人们真的对这种变化感到满意吗? 我对一个从未需要广播在容器上没有的形状,而我在广播应该所有的时间视为标量的东西。 我“修复”的每一个弃用警告都让我流泪。

我广播应该被视为标量的东西_一直_。

这些东西的种类是什么?

可以是任何东西。 例如,在定义优化模型类型Model和决策变量类型Variable ,您可能有x::Vector{Variable} ,您希望在求解模型model使用函数value(::Variable, ::Model)::Float64 。 以前,您可以这样做:

value.(x, model)

参数类型来自其他包的情况也经常出现,因此在这种情况下,为这些类型添加一个方法到broadcastable将是类型盗版。 所以你必须使用Ref或一个单元素元组。 这并非不可逾越,但在我看来,它只是使普通情况变得不那么优雅,以支持相对模糊的使用模式。

是的,我明白你的观点,我同意在这种情况下它很烦人。 也就是说,旧行为绝对有问题——这是“在某些情况下默认回退绝对是错误的”事情之一。

简而言之,有四个选项可以避免错误回退:

  1. 要求 _everything_ 实现一些描述它们如何广播的方法
  2. 默认将事物视为容器,并为非容器错误/弃用。

    • 我们将尝试iterate未知对象,这将导致标量错误

    • 标量有两个逃生舱口 - 用户可以在调用站点将它们包装起来,图书馆作者可以选择加入未包装的类标量广播。

  3. 默认将事物视为标量和未知容器的错误

    • 鉴于没有仅为标量定义的相关方法,我们必须断言iterate会引发方法错误。 那是缓慢而迂回的。

    • 只有一个可用于自定义容器的逃生舱不会出错:他们的库作者明确选择加入广播。 对于主要目的是使用容器的函数来说,这似乎很倒退。

  4. 检查applicable(iterate, …)并相应地切换行为

    • 由于 start/next/done 的弃用机制,这目前不起作用,并且通常对于将方法推迟到成员的包装器类型可能是错误的。

选项 1 对每个人都更糟,选项 2 是现状,选项 3 是倒退的,选项 4 是我们以前从未做过的并且可能有问题。

我想一些讨论一定是在幕后发生的,但我只是不相信我在这个线程和https://github.com/JuliaLang/julia/pull/25356中看到的反对nalimilan的论点stevengj的位置。

只有一个可用于自定义容器的逃生舱不会出错:他们的库作者明确选择加入广播。 对于主要目的是使用容器的函数来说,这似乎很倒退。

这是我的主要分歧点。 在我看来,在所有 Julia 代码中# of iterator types << # of types that should be treated as scalars in a broadcast situation < # of broadcast calls 。 因此,如果需要完成“额外”操作的次数与迭代器类型的数量而不是广播调用的数量成比例,我更喜欢它。 如果一个库作者定义了一个迭代器,要求他们再定义一个方法并不是完全不合理的,而要求每个包作者为他们所有的不可迭代类型定义Base.broadcastable(x) = Ref(x)是完全不合理的在高比例的broadcast调用中避免丑陋(恕我直言) Ref s。

我知道,有一个方法来实现它来定义迭代是好的,但它不是太多的工作更实现一个为任何一个新的特征,或者使指定它需要Base.iteratorsize一个新的迭代器(和摆脱有问题的HasLength默认值)。 后备broadcastable方法可以基于该特征。 或者,如果你真的很喜欢用单一方法定义迭代,你可以(post-deprecation-removal)将该显式特征默认为applicable(iterate, ...)https://github.com/JuliaLang/ julia/issues/18618#issuecomment -354618742,如果需要,只需覆盖该默认值。 如果需要,也可以通过进一步专门化broadcastable来处理像String这样的角落案例。

这实际上是 0.6 的设计,导致了这个问题以及 #26421 和 #19577 和 #23197 和 #23746 以及可能更多——搜索这个很困难。

这意味着 Base 提供了一个默认的回退,这对于整类对象是不正确的。 这就是为什么我更喜欢错误的机制,除非你选择加入,以一种或另一种方式。 这是自以为是,过渡是一种痛苦,但它迫使您明确。

您可能认为“类标量”自定义类型多于类似迭代器的自定义类型,但我坚持广播首先是对容器的操作这一事实。 我希望f.(x)做某种映射,而不仅仅是f(x)

最后,获得标量默认处理的容器根本无法与广播一起使用。 例如, String是一个集合类型,我们将其设置为像标量一样的特殊情况; 即使在某些情况下似乎有意义(例如, isletter.("a1b2c3") ),也无法“深入”并按元素进行工作。 这就是不对称的论点:与将collect容器放入实际可广播集合相比,您可以更有效地将容器包装在 Ref 中以将它们视为标量。

这些是主要论点。 至于Ref的丑陋,我完全同意。 一个解决方案是#27608。

很公平。 对于这些问题,我没有任何击倒论点或神奇的解决方案, https://github.com/JuliaLang/julia/pull/27608会改善情况。

@tkoolen我有同样的担忧和用例

@mbauman上面给出的论点可能并不完全令人信服。 这里有两个问题要更完整:

1) 可以使broadcastable成为任何可迭代对象的必需接口。
这将是完全系统的,并且会迫使开发人员考虑
他们的迭代器在广播下应该如何表现。
在大多数情况下,建议将其设置为collect(x)会使转换相对容易。
不会有任何性能损失,对吧?

2) 因此,如果x作为标量广播,则归结为f.(x)会出现错误的意愿。
为什么不是f.(x, y, z)的 linter 警告/错误,例如“'f' 的所有参数都作为标量广播”?

无论如何,修复#27563(例如通过#27608)并让用户在1.0 之前使用它可能是明智的。
[0.7 和 1.0.0-rc1.0 在没有修复的情况下发布]。

无论如何,修复#27563(例如通过#27608)并让用户在1.0 之前使用它可能是明智的。
[0.7 和 1.0.0-rc1.0 在没有修复的情况下发布]。

我想你错过了1.0 已经发布的消息。

@StefanKarpinski确实错过了。 祝贺所有开发人员,Julia 很棒,继续加油!

此页面是否有帮助?
0 / 5 - 0 等级