Julia: `map(func, x)` 的替代语法

创建于 2014-09-23  ·  283评论  ·  资料来源: JuliaLang/julia

这在此处进行了详细讨论。 我很难找到它,并认为它应该有自己的问题。

breaking speculative

最有用的评论

一旦三巨头的成员(Stefan、Jeff、Viral)合并 #15032(我认为它已准备好合并),我将关闭它并提交路线图问题以概述剩余的提议更改:修复广播类型计算,弃用@vectorize ,将.op变成广播糖,添加语法级别的“广播融合”,最后与就地分配融合。 最后两个可能不会进入0.5。

所有283条评论

+1

func.(args...)作为语法糖

broadcast(func, args...)

但也许我是唯一愿意这样做的人?
无论哪种方式,+1。

:-1: 如果有的话,我认为 Stefan 对f[...]其他建议与理解非常相似。

@ihnorton一样,我也不太喜欢这个想法。 特别是,我不喜欢同时拥有a .+ bsin.(a)的不对称性。

也许我们不需要特殊的语法。 使用#1470,我们可以做类似的事情

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

正确的? 也许这太神奇了,无法在任何功能上自动映射。

@quinnj那一行总结了我对允许呼叫重载的最大恐惧。 我将无法入睡几天。

还不确定它在语法上是否可行,但是.sin(x)呢? 这更类似于a .+ b吗?

我认为[]太重了,不能用于此目的。 例如,我们可能可以写Int(x) ,但Int[x]构造一个数组,因此不能表示map

我会加入.sin(x)

我们必须为此收回一些语法,但如果Int(x)是标量版本,那么Int[x]类比构造元素类型Int的向量是合理的。 在我看来,让语法更加连贯的机会实际上是f[v]提案最吸引人的方面之一。

map制作f[v]语法如何使语法更加连贯? 我不明白。 map具有与当前T[...]数组构造函数语法不同的“形状”。 Vector{Int}[...]呢? 那不行吗?

大声笑,对不起@JeffBezanson 的恐慌! 哈哈,调用重载确实有点吓人,每隔一段时间,我就会想到你可以在 julia 中进行的各种代码混淆,使用call ,你可以做一些粗糙的事情。

我认为.sin(x)听起来也是个好主意。 是否就如何处理多参数达成共识?

:-1:. 与使用高阶函数相比,节省几个字符我认为不值得在可读性方面付出代价。 你能想象一个文件到处都是.func() / func.()func()吗?

至少,我们似乎很可能会删除a.(b)语法。

哇,说起蜂巢吧! 我更改了名称以更好地反映讨论。

我们还可以将 2 参数map重命名为zipWith :)

如果确实需要某些语法,那么[f <- b]或括号内的其他双关语如何理解?

@JeffBezanson ,您只是害怕有人会编写 CJOS 或 Moose.jl :) ...如果我们获得该功能,只需将其放在手册的Don't do stupid stuff: I won't optimize that部分)

当前写入Int[...]表示您正在构造一个元素类型Int的数组。 但是,如果Int(x)意味着通过将Int作为函数应用将x转换为Int ,那么您也可以考虑Int[...]表示“应用Int到 ..." 中的每一件事,哦,顺便说一句,它恰好产生了Int类型的值。 所以写Int[v]将等价于[ Int(x) for x in v ]Int[ f(x) for x in v ]将等价于[ Int(f(x)) for x in v ] 。 当然,那么你一开始就失去了编写Int[ f(x) for x in v ]的一些实用性——即我们可以静态地知道元素类型是Int ——但是如果强制执行Int(x)必须产生Int类型的值(不是不合理的约束),然后我们才能恢复该属性。

让我印象深刻的是更多的矢量化/隐式猫地狱。 Int[x, y]会做什么? 或者更糟糕的是, Vector{Int}[x]

我并不是说它是有史以来最好的想法,甚至没有提倡它——我只是指出它不会_完全_与现有用法发生冲突,这本身就是一种黑客行为。 如果我们可以让现有的用法成为更连贯的模式的一部分,那将是一场胜利。 我不确定f[v,w]是什么意思——显而易见的选择是[ f(x,y) for x in v, y in w ]map(f,v,w) ,但还有更多选择。

感觉a.(b)用的很少。 进行了快速测试,它仅在 4,000 个 julia 源文件中的 54 个中使用: https://gist.github.com/jakebolewski/104458397f2e97a3d57d。

我认为它确实完全冲突。 T[x]具有“形状” T --> Array{T} ,而map具有形状Array{T} --> Array{S} 。 这些几乎是不相容的。

为此,我认为我们必须放弃T[x,y,z]作为Vector{T}的构造函数。 普通的旧数组索引, A[I]其中I是一个向量,可以看作map(i->A[i], I) 。 “应用”一个数组就像应用一个函数(当然,matlab 甚至对它们使用相同的语法)。 从这个意义上说,语法确实有效,但在此过程中我们会丢失类型向量语法。

我觉得在这里辩论语法会分散更重要的变化:使map更快。

显然,让map变得更快(顺便说一下,这需要成为对 julia 中函数概念进行相当彻底的重新设计的一部分)更重要。 然而,从可用性的角度来看,从sin(x)map(sin, x)非常重要,因此要真正杀死向量化,语法非常重要。

然而,从可用性的角度来看,从 sin(x) 到 map(sin, x) 非常重要,因此要真正消除向量化,语法非常重要。

完全同意。

我同意@JeffBezanson的观点,即f[x]与当前的类型化数组结构Int[x, y]等几乎不可调和。

另一个更喜欢.sin而不是sin.的原因是最终允许使用例如Base.(+)来访问 $#$ Base $#$ 中的+函数(一次a.(b)被删除)。

当一个模块定义了它自己的sin (或其他)函数并且我们想在向量上使用该函数时,我们是否执行Module..sin(v)Module.(.sin(v)) ? Module.(.sin)(v) ? .Module.sin(v)

这些选项中没有一个看起来真的很好。

我觉得这个讨论错过了问题的实质。 即:当将单参数函数映射到容器时,我觉得map(func, container)语法_已经_清晰简洁。 相反,我认为只有在处理多个参数时,我们才能从更好的柯里化语法中受益。

map(x->func(x,other,args), container)的冗长为例,或者链接过滤器操作以使其更糟filter(x->func2(x[1]) == val, map(x->func1(x,other,args), container))

在这些情况下,我觉得缩短的 map 语法没有多大帮助。 并不是说我认为这些特别糟糕,但是 a) 我认为简写map不会有太大帮助 b) 我喜欢在 Haskell 的一些语法之后苦苦挣扎。 ;)

IIRC,在 Haskell 中,上面的内容可以写成filter ((==val) . func2 . fst) $ map (func1 other args) container ,只是对func1的参数顺序略有改变。

在 elm 中, .funcx->x.func定义,这非常有用,请参阅elm 记录。 在将这种语法用于map之前,应该考虑这一点。

我喜欢。

尽管字段访问在 Julia 中并不像在许多语言中那样重要。

是的,这里感觉不太相关,因为 Julia 中的字段更适合“私人”使用。 但是随着关于重载字段访问的持续讨论,这可能变得更加明智。

f.(x)看起来像问题较少的解决方案,如果不是因为.+的不对称性。 但是,恕我直言,保持.与“元素操作”的符号关联是一个好主意。

如果可以弃用当前的类型化数组构造,则可以将func[v...]转换为map(func, v...) ,然后可以将文字数组写入T[[a1, ..., an]] (而不是当前的T[a1, ..., an] )。

我发现sin∘v也很安静(当数组v被视为从索引到包含值的应用程序时),或者更简单的是sin*vv*[sin]' (这需要定义*(x, f::Callable) ) 等。

重新回到这个问题上,我意识到f.(x)可以被视为一种非常自然的语法。 您可以将其读取为f.( ,而不是将其读取为f.( #$ 。 然后.(隐喻地是(函数调用运算符的元素明智版本,它与.+和朋友完全一致。

.(是一个函数调用运算符的想法让我非常难过。

@johnmyleswhite需要详细说明吗? 我说的是语法的直观性,或者它与其他语言的视觉一致性,而不是技术实现。

对我来说, (根本不是语言语义的一部分:它只是语法的一部分。 所以我不想为.((发明一种方法来开始不同。 前者会生成multicall Expr而不是call Expr吗?

不。正如我所说,我根本没有暗示应该有两个不同的呼叫运营商。 只是试图为元素操作找到一个_视觉上_一致的语法。

对我来说,扼杀这些选项的是如何向量化多参数函数的问题。 没有单一的方法可以做到这一点,任何足以支持所有可能方式的通用方法开始看起来很像我们已经拥有的多维数组推导式。

多参数map迭代所有参数是非常标准的。
如果我们这样做,我会使用 .( 调用 map 的语法。该语法可能
由于各种原因不是那么好,但我会在这些方面很好。

多参数函数可以进行多种泛化这一事实不能成为反对至少支持某些特殊情况的论据——就像矩阵转置一样,即使它可以以多种方式对张量进行泛化也是有用的。

我们只需要选择最有用的解决方案。 可能的选择已经在这里讨论过: https ://github.com/JuliaLang/julia/issues/8389#issuecomment -55953120(以及以下评论)。 正如@JeffBezanson所说,当前map的行为是合理的。 一个有趣的标准是能够替换@vectorize_2arg

我的观点是让sin.(x)x .+ y共存很尴尬。 我宁愿有.sin(x) -> map(sin, x)x .+ y -> map(+, x, y)

.+实际上使用broadcast

出于纯粹的绝望,还有其他一些想法:

  1. 重载冒号sin:x 。 不能很好地推广到多个参数。
  2. sin.[x] --- 此语法可用,目前无意义。
  3. sin@x --- 不可用,但可能有

我真的不相信我们需要这个。

我也不。 我认为f.(x)是这里最好的选择,但我不喜欢它。

但是如果没有这个,我们怎么能避免创建各种矢量化函数,特别是像int()这样的东西? 这就是促使我在https://github.com/JuliaLang/julia/issues/8389 开始讨论的原因。

我们应该鼓励人们使用map(func, x) 。 打字不多,任何来自另一种语言的人都可以立即清楚地看到。

当然,请确保它很快。

是的,但是对于交互式使用,我觉得很痛苦。 对于来自 R 的人来说,这将是一个主要的烦恼(至少,我不了解其他语言),而且我不希望这样给人一种 Julia 不适合数据分析的感觉。

另一个问题是一致性:除非您愿意删除所有当前的矢量化函数,包括logexp等,并要求人们改用map (这可能是对这个决定的实用性的一个有趣的测试),语言将是不一致的,因此很难提前知道一个函数是否是向量化的(以及在哪些参数上)。

许多其他语言多年来一直在使用map

据我了解停止对所有内容进行矢量化的计划,删除大多数/所有矢量化函数始终是策略的一部分。

是的,当然,我们会停止对所有内容进行矢量化。 不一致已经存在:已经很难知道哪些函数已经或应该被向量化,因为没有真正令人信服的理由为什么sinexp等应该隐式映射到数组上。

告诉库编写者将@vectorize放在所有适当的函数上是愚蠢的; 您应该能够只编写一个函数,如果有人想为他们使用的每个元素计算它map

让我们想象一下如果我们删除常用的向量化数学函数会发生什么:

  1. 就个人而言,我不介意用map(exp, x)代替exp(x) ,尽管后者稍微短一些并且更简洁。 但是,性能存在很大差异。 矢量化函数比我机器上的地图快约 5 倍。
  2. 当您使用复合表达式时,问题会更有趣。 考虑一个复合表达式: exp(0.5 * abs2(x - y)) ,那么我们有
# baseline: the shortest way
exp(0.5 * abs2(x - y))    # ... takes 0.03762 sec (for 10^6 elements)

# using map (very cumbersome for compound expressions)
map(exp, 0.5 * map(abs2, x - y))   # ... takes 0.1304 sec (about 3.5x slower)

# using anonymous function (shorter for compound expressions)
map((u, v) -> 0.5 * exp(abs2(u - v)), x, y)   # ... takes 0.2228 sec (even slower, about 6x baseline)

# using array comprehension (we have to deal with two array arguments)

# method 1:  using zip to combine the arguments (readability not bad)
[0.5 * exp(abs2(u - v)) for (u, v) in zip(x, y)]  # ... takes 0.140 sec, comparable to using map

# method 2:  using index, resulting in a slightly longer statement
[0.5 * exp(abs2(x[i] - y[i])) for i = 1:length(x)]  # ... takes 0.016 sec, 2x faster than baseline 

如果我们要删除矢量化数学函数,那么在可读性和性能方面都可接受的唯一方法似乎是数组理解。 尽管如此,它们还是不如矢量化数学方便。

-1 用于删除矢量化版本。 事实上,VML 和 Yeppp 等库为矢量化版本提供了更高的性能,我们需要弄清楚如何利用这些。

这些是否在基础上是一个不同的讨论和一个更大的讨论,但需求是真实的,性能可能比我们拥有的更高。

@lindahua@ViralBShah :您的一些担忧似乎是基于我们在改进map之前会摆脱矢量化函数的假设,但我不相信有人提议这样做。

我认为@lindahua的例子很能说明问题:向量化语法比其他解决方案更好,更接近数学公式。 失去它会很糟糕,来自其他科学语言的人可能会认为这是 Julia 的负面影响。

我可以删除所有矢量化函数(当map足够快时),看看它是如何进行的。 我相信在那个时候提供一种方便语法的兴趣会更加明显,如果事实证明是这样的话,仍然是时候添加它了。

我认为 Julia 与许多其他语言不同,因为它强调交互式使用(在这种情况下输入较长的表达式很烦人)和数学计算(公式应尽可能接近数学表达式以使代码可读)。 这就是 Matlab、R 和 Numpy 提供矢量化函数的部分原因(另一个原因当然是性能,这个问题在 Julia 中可以解决)。

我从讨论中的感觉是向量化数学的重要性被低估了。 实际上,向量化数学的主要优势之一在于表达的简洁性——它不仅仅是“for-loop 慢的语言”的权宜之计。

比较y = exp(x)

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

前者显然比后者更简洁易读。 Julia 使循环高效这一事实并不意味着我们应该总是对代码进行去矢量化,对我来说,这会适得其反。

我们应该鼓励人们以自然的方式编写代码。 一方面,这意味着我们不应该尝试在不适合的上下文中编写复杂的代码和扭曲向量化函数; 另一方面,我们应该支持在最有意义的时候使用矢量化数学。

在实践中,将公式映射到数字数组是一种非常常见的操作,我们应该努力使其方便而不是繁琐。 为此,矢量化代码仍然是最自然和简洁的方式。 抛开性能不谈,它们仍然比调用map函数要好,尤其是对于具有多个参数的复合表达式。

我们当然不想要所有东西的矢量化版本,但是由于上面提到的大华原因,每次都使用 map 进行矢量化会很烦人。

如果 map 速度很快,它肯定会让我们专注于拥有一组更小、更有意义的矢量化函数。

我必须说我强烈支持简洁的地图符号。 我觉得这是不同需求之间的最佳折衷。

我不喜欢矢量化函数。 它隐藏了正在发生的事情。 这种创建函数矢量化版本的习惯导致了神秘代码。 假设您有一些代码,其中在向量上调用了包中的函数f 。 即使您对函数的作用有所了解,您也无法通过阅读代码来确定它是按元素执行还是对整个向量起作用。 如果科学计算语言没有拥有这些矢量化函数的历史,我认为我们现在几乎不会接受它们。

它还导致一种情况,即隐含地鼓励您编写函数的矢量化版本,以便在使用函数的地方启用简洁的代码。

关于正在发生的事情最明确的代码是循环,但正如@lindahua所说,它最终变得非常冗长,这有其自身的缺点,尤其是在一种也用于交互使用的语言中。

这导致了map妥协,我觉得这更接近理想,但我仍然同意@lindahua的观点,即它不够简洁。

由于我前面提到的原因,我不同意@lindahua的地方是矢量化函数是最佳选择。 我的推理导致 Julia 应该有一个非常简洁的符号map

我发现 Mathematica 是如何用它的速记法做到这一点的,真的很吸引人。 在 Mathematica 中将函数应用于参数的简写符号是@ ,因此您将Apply函数f到向量为: f @ vector . 映射函数的相关简写符号是/@ ,因此您将f映射到向量为: f /@ vector 。 这有几个吸引人的特点。 两种短手都很简洁。 两者都使用@符号这一事实强调了它们所做的事情之间存在关系,但是 map 中的/仍然可以在视觉上区分,以便在您映射时以及何时不是。 这并不是说 Julia 应该盲目地复制 Mathematica 的符号,只是这种好的映射符号非常有价值

我并不是建议摆脱所有矢量化函数。 那列火车早就离开了车站。 相反,我建议保持向量化函数列表尽可能短,并提供一个很好的简洁映射符号来阻止添加到向量化函数列表中。

当然,所有这些都取决于map和匿名函数的速度。 现在朱莉娅处于一个奇怪的位置。 过去,在科学计算语言中,函数被向量化是因为循环很慢。 这不是问题。 相反,在 Julia 中,您将函数向量化,因为 map 和匿名函数很慢。 所以我们回到了我们开始的地方,但出于不同的原因。

矢量化库函数有一个缺点——只有库明确提供的那些函数才可用。 也就是说,例如, sin(x)应用于向量时很快,而sin(2*x)突然慢得多,因为它需要一个需要遍历两次的中间数组(第一次写入,然后读取)。

一种解决方案是矢量化数学函数库。 这些将是 LLVM 可用于内联的sincos等的实现。 然后 LLVM 可以向量化这个循环,并有望产生非常高效的代码。 Yeppp 似乎有正确的循环内核,但似乎没有将它们公开用于内联。

矢量化作为范例的另一个问题是,如果您使用标准库所支持的其他容器类型,它根本不起作用。 您可以在 DataArrays 中看到这一点:有大量的元编程代码用于为我们正在定义的新容器类型重新向量化函数。

将其与@eschnett的观点相结合,您将得到:

  • 仅当您将自己限制为标准库中的函数时,矢量化函数才有效
  • 仅当您将自己限制为标准库中的容器类型时,矢量化函数才有效

我想澄清一下,我的观点不是我们应该始终保留矢量化函数,而是我们需要一种与编写矢量化函数一样简洁的方法。 使用map可能不能满足这一点。

我确实喜欢@eschnett将某些函数标记为 _vectorizable_ 的想法,并且编译器可以自动将可向量化函数映射到数组,而无需用户显式定义向量化版本。 编译器还可以将可向量化函数链融合到一个融合循环中。

这是我的想法,灵感来自@eschnett的评论:

# The <strong i="11">@vec</strong> macro tags the function that follows as vectorizable
<strong i="12">@vec</strong> abs2(x::Real) = x * x
<strong i="13">@vec</strong> function exp(x::Real) 
   # ... internal implementation ...
end

exp(2.0)  # simply calls the function

x = rand(100);
exp(x)    # maps exp to x, as exp is tagged as vectorizable

exp(abs2(x))  # maps v -> exp(abs2(v)), as this is applying a chain of vectorizable functions

编译器还可以通过识别使用 SIMD 的机会来重新向量化计算(在较低级别)。

当然, @vec应该对最终用户可用,以便人们可以将自己的函数声明为可向量化的。

谢谢, @lindahua :你的澄清很有帮助。

@vec与声明函数@pure $ 会有所不同吗?

@vec表示该函数可以按元素方式映射。

并非所有纯函数都属于这一类,例如sum是纯函数,我认为不建议将其声明为 _vectorizable_。

无法从pure和 + 上的+ associative标记中重新导出 $ sumvec属性以及有关reduce的知识foldl / foldr在给定pureassociative函数时工作? 显然,这都是假设性的,但是如果 Julia 全力以赴为类型提供特征,我可以想象通过也全力以赴地为函数提供特征,从而显着提高矢量化的最新技术。

我觉得添加新语法与我们想要的相反(在清除 Any[] 和 Dict 的特殊语法之后)。 删除这些向量化函数的全部要点是减少特殊情况(我认为语法不应与函数语义不同)。 但我同意简洁的地图会很有用。

那么为什么不添加一个简洁的中缀map运算符呢? 这里我随便挑$ 。 这将使@lindahua的示例从

exp(0.5 * abs2(x - y))

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

现在,如果我们只为用户定义的中缀运算符提供类似 Haskell 的支持,那么这将只是一行更改($) = map 。 :)

IMO,其他语法建议在视觉上过于接近现有语法,并且在查看代码时需要进一步的脑力分析:

  • foo.(x) -- 视觉上类似于标准类型成员访问
  • foo[x] -- 我是在访问 foo 数组的第 x 个成员还是在这里调用 map?
  • .foo(x) - 正如@kmsquire指出的那样有问题

而且我觉得@vec解决方案与我们一开始就试图避免的@vectorize太接近了。 当然,一些注释 ala #8297 会很好,并且可以帮助未来,更智能的编译器可以识别这些流融合机会并相应地进行优化。 但我不喜欢强迫它的想法。

如果允许您执行以下操作,中缀映射加上快速匿名函数也可以帮助创建临时对象:

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

我想知道是否可以在指定一个可向量化函数的上下文中借鉴酷炫的新Trait.jl中的想法。 当然,在这种情况下,我们正在查看Function类型的单个 _instances_ 是否可矢量化,而不是具有特定特征的 julia 类型。

现在,如果我们只对用户定义的中缀运算符有类似 Haskell 的支持

6582 #6929 不够用?

这个讨论中有一点关于使用尽可能少的数组临时向量化整个表达式。 想要矢量化语法的用户不会只想要矢量化exp(x) ; 他们会想写像

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

并让它神奇地矢量化

没有必要将函数标记为“可向量化”。 这更像是函数如何实现和 Julia 可用的属性。 如果它们是用 C 实现的,那么它们需要被编译为 LLVM 字节码(不是目标文件),以便 LLVM 优化器仍然可以访问它们。 在 Julia 中实现它们也可以。

可向量化意味着以 Yeppp 项目很好地描述的方式实现该功能:没有分支、没有表、除法或平方根,如果它们在硬件中可用作向量指令,否则许多融合乘加运算和向量合并操作。

不幸的是,这样的实现将依赖于硬件,即人们可能不得不根据哪些硬件指令是有效的来选择不同的算法或不同的实现。 我过去在 C++ 中做过这个(https://bitbucket.org/eschnett/vecmathlib/wiki/Home),目标受众略有不同(基于模板的操作是手动矢量化而不是自动矢量化编译器)。

在 Julia 中,事情会更容易,因为 (a) 我们知道编译器将是 LLVM,并且 (b) 我们可以在 Julia 中实现它而不是 C++(宏与模板)。

还有一件事需要考虑:如果放弃部分 IEEE 标准,那么可以大大提高速度。 许多用户知道,例如非规范化数字并不重要,或者输入总是小于sqrt(max(Double))等。问题是是否为这些情况提供快速路径。 我知道我会对此非常感兴趣,但其他人可能更喜欢完全可重复的结果。

让我在 Julia 中制作一个可矢量化的exp原型。 然后我们可以看到 LLVM 在矢量化循环方面的表现,以及我们获得的速度。

函数的参数用全角括号是不是太吓人了~

抱歉,我没有意识到我在重复@johnmyleswhite上面所说的完全相同的事情,即 re function with trait。 继续。

@eschnett我认为将API(函数是否可矢量化)链接到实现细节(函数的编译方式)是不合理的。 理解并在时间和架构之间保持稳定听起来相当复杂,并且在调用外部库中的函数时不起作用,例如log不会被检测为可向量化,因为从 openlibm 调用函数。

OTOH @johnmyleswhite使用特征来传达函数的数学属性的想法可能是一个很好的解决方案。 ( @lindahua的提议是我不久前在某处提出的一个功能,但使用特征的解决方案可能会更好。)

现在,如果我们只对用户定义的中缀运算符有类似 Haskell 的支持

6582 #6929 不够用?

我应该说: ... 用户定义的 _non-unicode_ 中缀运算符,因为我认为我们不想要求用户输入 unicode 字符来访问这样的核心功能。 虽然我看到$实际上是添加的其中之一,所以谢谢你! 哇,所以这实际上在 Julia _today_ 中有效(即使它不是“快”......但是):

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

我不知道这是否是map的最佳选择,但将$用于xor确实看起来很浪费。 按位异或并不经常使用。 map更重要。

@jiahao上面的观点非常好:像exp这样的单个矢量化函数实际上是一种获取矢量化_expressions_(如exp(-x^2) )的技巧。 像@devec这样的语法将非常有价值:您将获得去矢量化的性能以及不需要单独将函数识别为矢量化的一般性。

为此使用函数特征的能力会很酷,但我仍然觉得它不太令人满意。 一般来说,真正发生的是一个人编写一个函数,另一个人迭代它。

我同意这不是函数的属性,而是函数使用的属性。 关于应用特征的讨论似乎是在找错树。

头脑风暴:如何标记要映射的参数,以便支持多参数映射:

a = split("the quick brown")
b = split("fox deer bear")
c = split("jumped over the lazy")
d = split("dog cat")
e = string(a, " ", b., " ", c, " ", d.) # -> 3x2 Vector{String} of the combinations   
# e[1,1]: """["the","quick", "brown"] fox ["jumped","over","the","lazy"] dog"""

我不确定.bb.是否更好地表明您想要映射。 在这种情况下,我喜欢返回多维 3x2 结果,因为它代表map ping 的形状。

格伦

这里https://github.com/eschnett/Vecmathlib.jl是一个带有示例的 repo
exp的实现,以可以由 LLVM 优化的方式编写。
此实现大约是标准exp的两倍
在我的系统上实施。 它(可能)还没有达到 Yeppp 的速度,
可能是因为 LLVM 不会将相应的 SIMD 循环展开为
像耶普一样积极进取。 (我比较了反汇编的指令。)

编写可向量化的exp函数并不容易。 使用它看起来像这样:

function kernel_vexp2{T}(ni::Int, nj::Int, x::Array{T,1}, y::Array{T,1})
    for j in 1:nj
        <strong i="16">@simd</strong> for i in 1:ni
            <strong i="17">@inbounds</strong> y[i] += vexp2(x[i])
        end
    end
end

j循环和函数参数仅用于
基准测试目的。

Julia 有@unroll宏吗?

-埃里克

在 2014 年 11 月 2 日星期日晚上 8:26,Tim Holy [email protected]写道:

我同意这不是函数的属性,而是
函数的使用。 关于应用特征的讨论似乎是
叫错树的情况。

直接回复此邮件或在 GitHub 上查看
https://github.com/JuliaLang/julia/issues/8450#issuecomment -61433026。

埃里克·施奈特[email protected]
http://www.perimeterinstitute.ca/personal/eschnetter/

exp这样的单个矢量化函数实际上是一种获取矢量化 _expressions_ 的技巧,例如exp(-x^2)

从标量域提升整个表达式的核心语法将非常有趣。 向量化只是一个例子(目标域是向量); 另一个有趣的用例是提升到语义完全不同的矩阵域(#5840)。 在矩阵域中,探索不同表达式的调度如何工作也很有用,因为在一般情况下,如果你想要像sqrtm这样更简单的算法,你会想要 Schur-Parlett 和其他更专业的算法。 (通过巧妙的语法,您可以完全摆脱*m函数 - expmlogmsqrtm ,...)

Julia 有@unroll宏吗?

使用 Base.Cartesian
@nexpr 4 d->(y[i+d] = exp(x[i+d])

(如果您有任何问题,请参阅 http://docs.julialang.org/en/latest/devdocs/cartesian/。)

@jiahao 将此推广到矩阵函数听起来像是一个有趣的挑战,但我对此的了解接近于零。 你对它的工作方式有什么想法吗? 这将如何与矢量化结合起来? 语法如何允许在向量/矩阵上按元素应用exp和计算其矩阵指数之间的区别?

@timholy :谢谢! 我没想过使用笛卡尔展开。

不幸的是, @nexprs (或手动展开)生成的代码不再矢量化。 (这是 LLVM 3.3,也许 LLVM 3.5 会更好。)

回复:展开,另见@toivoh关于 julia-users 的帖子。 也值得一试#6271。

@nalimilan我还没有考虑到这一点,但是使用单个matrixfunc函数(例如)来实现标量->矩阵提升将非常简单。 一种假设的语法(完全虚构)可能是

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

那么这将

  1. X类型Matrix{Float64}并具有元素(类型参数) Float64 (因此隐式定义Float64 => Matrix{Float64}提升)识别提升的源域和目标域, 然后
  2. 调用matrixfunc(x->exp(c*x^2)*sin(x), X)来计算expm(c*X^2)*sinm(X)的等价物,但要避免矩阵相乘。

在其他一些代码中X可能是Vector{Int}并且隐含的提升将从IntVector{Int} ,然后lift(x->exp(c*x^2)*sin(x), X)可以打电话给map(x->exp(c*x^2)*sin(x), X)

还可以想象其他明确指定源域和目标域的方法,例如lift(Number=>Matrix, x->exp(c*x^2)*sin(x), X)

@nalimilan矢量化并不是 API 的真正属性。 使用当今的编译器技术,只有内联的函数才能被向量化。 事情主要取决于函数实现——如果它以“正确的方式”编写,那么编译器可以对其进行矢量化(在将其内联到周围的循环中之后)。

@eschnett :您是否使用与其他人相同的矢量化含义? 听起来你是关于 SIMD 等的,这不是我理解@nalimilan的意思。

正确的。 这里有两种不同的矢量化概念。 一笔交易
为紧密的内部循环(处理器矢量化)获取 SIMD。 主要的
这里讨论的问题是某种“自动”的语法/语义
能够在集合上调用单个(或多个)参数函数。

2014 年 11 月 4 日星期二晚上 7:04,John Myles White [email protected]
写道:

@eschnett https://github.com/eschnett :你用的是同一个意思吗
像其他人一样矢量化? 听起来你是关于 SIMD 等的,其中
不是我所理解的@nalimilan https://github.com/nalimilan
意思是。


直接回复此邮件或在 GitHub 上查看
https://github.com/JuliaLang/julia/issues/8450#issuecomment -61738237。

与其他.运算符对称, f.(x)不应该将函数集合应用于值集合吗? (例如,用于从某个单位坐标系转换为物理坐标。)

在讨论语法时,出现了使用显式循环来表达map(log, x)等价物的想法太慢了。 因此,如果可以使这个速度足够快,那么调用map (或使用特殊语法)或编写循环在语义级别上是等效的,并且不需要引入语法消歧。 目前,调用向量对数函数比在数组上编写循环要快得多,这促使人们寻求一种在代码中表达这种区别的方法。

这里有两个层面的问题:(1)语法和语义,(2)实现。

语法和语义问题是关于用户如何表达将某些计算以元素/广播方式映射到给定数组的意图。 目前,Julia 支持两种方式:使用向量化函数和允许用户显式编写循环(有时在宏的帮助下)。 这两种方式都不理想。 虽然向量化函数允许人们编写非常简洁的表达式,例如exp(0.5 * (x - y).^2) ,但它们有两个问题:(1) 很难划清哪些函数应该提供向量化版本,哪些不应该提供向量化版本,因此通常会导致在开发人员方面的无休止的辩论和用户方面的混乱中(您经常需要查看文档以确定某些功能是否被矢量化)。 (2) 跨函数边界融合循环变得困难。 在这一点上,可能在未来几个月/几年内,编译器可能无法执行诸如同时查看多个函数、识别联合数据流以及生成跨函数边界的优化代码路径等复杂任务。

使用map函数可解决此处的问题 (1)。 然而,这仍然没有为解决问题 (2) 提供任何帮助——使用函数,无论是特定的矢量化函数还是通用的map ,总是会创建一个阻碍循环融合的函数边界,这在高性能计算中至关重要。 使用 map 函数也会导致冗长,例如,上面的表达式现在变成了更长的语句map(exp, 0.5 * map(abs2, x - y)) 。 你可以合理地想象这个问题会随着更复杂的表达而加剧。

在此线程中概述的所有建议中,我个人认为使用特殊符号表示映射是最有前途的方法。 首先,它保持了表达式的简洁性。 以 $-notation 为例,上面的表达式现在可以写成exp $(0.5 * abs2$(x - y)) 。 这比原始的向量化表达式要长一点,但也不算太糟——它所需要的只是在每次调用映射时插入一个$ 。 另一方面,这个符号也可以作为一个正在执行的映射的明确指示,编译器可以利用它来打破函数边界并产生一个融合循环。 在本课程中,编译器不必查看函数的内部实现——它只需要知道函数将映射到给定数组的每个元素。

鉴于现代 CPU 的所有功能,尤其是 SIMD 的能力,将多个循环融合为一个只是迈向高性能计算的一步。 此步骤本身不会触发 SIMD 指令的使用。 好消息是我们现在有了@simd宏。 当编译器认为这样做安全且有益时,可以将此宏插入到生成循环的开头。

总而言之,我认为 $-notation(或类似提议)可以在很大程度上解决语法和语义问题,同时为编译器提供必要的信息来融合循环和利用 SIMD,从而生成高性能代码。

@lindahua的总结是一个很好的恕我直言。

但我认为进一步扩展这一点会很有趣。 Julia 值得拥有一个雄心勃勃的系统,该系统将使许多常见模式与展开循环一样高效。

  • 将嵌套函数调用融合到一个循环中的模式也应该应用于运算符,以便A .* B .+ C不会导致创建两个临时对象,而只会创建一个结果。
  • 还应处理逐元素函数和归约的组合,以便在计算每个元素的值后即时应用归约。 通常,这将允许摆脱sumabs2(A) ,将其替换为像sum(abs$(A)$^2) (或sum(abs.(A).^2) )这样的标准符号。
  • 最后,非标准数组应该支持非标准迭代模式,这样对于稀疏矩阵A .* B只需要处理非零条目,并返回一个稀疏矩阵。 如果您想将逐元素函数应用于SetDict甚至Range ,这也会很有用。

最后两点可以通过使逐元素函数返回一个特殊的AbstractArray类型来工作,比如说LazyArray ,它将即时计算其元素(类似于Transpose从 https://github.com/JuliaLang/julia/issues/4774#issuecomment-59422003 键入)。 但是,不是通过使用从1length(A)的线性索引来天真地访问它的元素,而是可以使用迭代器协议。 给定类型的迭代器将根据类型的存储布局自动选择是按行迭代还是按列迭代是最有效的。 对于稀疏矩阵,它将允许跳过零条目(原始和结果需要具有共同的结构,参见 https://github.com/JuliaLang/julia/issues/7010、https://github. com/JuliaLang/julia/issues/7157)。

当不应用缩减时,与原始对象具有相同类型和形状的对象将简单地通过迭代LazyArray (相当于collect ,但尊重原始数组的类型)来填充)。 唯一需要的是迭代器返回一个对象,该对象可用于在LazyArray getindex并在结果上调用 $ setindex! (例如线性或笛卡尔坐标)。

当应用缩减时,它会在其参数上使用相关的迭代方法来迭代LazyArray的所需维度,并用结果填充数组(相当于reduce但使用自定义迭代器以适应数组类型)。 一个函数(上一段中使用的那个)将返回一个迭代器,以最有效的方式遍历所有元素; 其他人将允许在特定维度上这样做。

整个系统还将非常直接地支持就地操作。

我正在考虑解决语法问题,并想到.=将元素操作应用于数组。
因此, @nalimilan的示例sum(abs.(A).^2))不幸的是必须分两步编写:

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

这将具有易于阅读的优点,并且意味着只需要为单个(或多个)输入编写元素函数并针对这种情况进行优化,而不是编写特定于数组的方法。

当然,除了性能和熟悉度之外,没有什么能阻止任何人现在简单地编写map((x) -> abs(x)^2, A) ,正如前面所述。

或者,可以使用.()包围要映射的表达式。
我不知道这样做会有多困难,但.sin(x).(x + sin(x))然后会将表达式映射到括号内或.后面的函数。
然后,这将允许像@nalimilan的示例那样减少sum(.(abs(A)^2))然后可以写在一行中。

这两个提议都使用了.前缀,在内部使用广播时,这让我想到了数组上的元素操作。 这可以很容易地用$或其他符号换掉。
这只是在每个要映射的函数周围放置一个映射运算符的替代方法,而是将整个表达式分组并指定要映射的函数。

我已经尝试了我在上一条评论中公开的LazyArray想法: https ://gist.github.com/nalimilan/e737bc8b3b10288abdad

这个概念证明没有任何语法糖,但(a ./ 2).^2将被翻译成要点中写的LazyArray(LazyArray(a, /, (2,)), ^, (2,)) 。 该系统运行良好,但需要更多优化才能在性能方面与循环竞争。 (预期的)问题似乎是第 12 行的函数调用没有优化(几乎所有分配都发生在那里),即使在不允许附加参数的版本中也是如此。 我想我需要在它调用的函数上对LazyArray进行参数化,但我还没有弄清楚如何做到这一点,更不用说处理参数了。 有任何想法吗?

关于如何提高LazyArray性能的任何建议?

@nalimilan一年前我尝试了一种类似的方法,在 NumericFuns 中使用函子类型来参数化惰性表达式类型。 我尝试了各种技巧,但没有遇到缩小性能差距的运气。

编译器优化在过去一年中逐渐得到改善。 但是我仍然觉得它仍然无法优化掉不必要的开销。 这种事情需要编译器积极的内联函数。 您可以尝试使用@inline看看它是否能让事情变得更好。

@lindahua @inline对时间没有任何影响,这对我来说是合乎逻辑的,因为getindex(::LazyArray, ...)专用于给定的LazyArray签名,它没有指定应该使用哪个函数叫做。 我需要类似LazyArray{T1, N, T2, F}的东西,使用 F 应该调用的函数,以便在编译getindex时知道调用。 有没有办法做到这一点?

内联将是另一个很大的改进,但目前时间甚至比非内联调用还要糟糕得多。

您可以考虑使用NumericFunsF可以是函子类型。

大华

我需要知道分布式返回类型的函数
计算,我在结果之前创建对结果的引用(和
因此它的类型)是已知的。 我自己实现了一个非常相似的东西,并且
可能应该改用你所谓的“函子”。 (我不喜欢
命名为“函子”,因为它们通常是别的东西<
http://en.wikipedia.org/wiki/Functor>,但我猜 C++ 搞混了
这里。)

我认为将 Functor 部分与
数学函数。

-埃里克

2014 年 11 月 20 日星期四上午 10:35,林大华通知@github.com
写道:

您可以考虑使用 NumericFuns 并且 F 可以是仿函数类型。

直接回复此邮件或在 GitHub 上查看
https://github.com/JuliaLang/julia/issues/8450#issuecomment -63826019。

埃里克·施奈特[email protected]
http://www.perimeterinstitute.ca/personal/eschnetter/

@lindahua我尝试过使用仿函数,确实性能要合理得多:
https://gist.github.com/nalimilan/d345e1c080984ed4c89a

With functions:
# elapsed time: 3.235718017 seconds (1192272000 bytes allocated, 32.20% gc time)

With functors:
# elapsed time: 0.220926698 seconds (80406656 bytes allocated, 26.89% gc time)

Loop:
# elapsed time: 0.07613788 seconds (80187556 bytes allocated, 45.31% gc time) 

我不确定可以做些什么来改进事情,因为生成的代码似乎还不是最优的。 我需要更多专家的眼睛来判断问题所在。

实际上,上面的测试使用Pow ,这显然会产生很大的速度差异,具体取决于您是编写显式循环还是使用LazyArray 。 我想这与只在后一种情况下执行的指令融合有关。 相同的现象是可见的,例如添加。 但是对于其他函数,差异要小得多,无论是使用 100x100 还是 1000x1000 矩阵,可能是因为它们是外部的,因此内联不会获得太多收益:

# With sqrt()
julia> test_lazy!(newa, a);
julia> <strong i="8">@time</strong> for i in 1:1000 test_lazy!(newa, a) end
elapsed time: 0.151761874 seconds (232000 bytes allocated)

julia> test_loop_dense!(newa, a);
julia> <strong i="9">@time</strong> for i in 1:1000 test_loop_dense!(newa, a) end
elapsed time: 0.121304952 seconds (0 bytes allocated)

# With exp()
julia> test_lazy!(newa, a);
julia> <strong i="10">@time</strong> for i in 1:1000 test_lazy!(newa, a) end
elapsed time: 0.289050295 seconds (232000 bytes allocated)

julia> test_loop_dense!(newa, a);
julia> <strong i="11">@time</strong> for i in 1:1000 test_loop_dense!(newa, a) end
elapsed time: 0.191016958 seconds (0 bytes allocated)

所以我想找出为什么LazyArray不会发生优化。 对于简单的操作,生成的程序集相当长。 例如,对于x/2 + 3

julia> a1 = LazyArray(a, Divide(), (2.0,));

julia> a2 = LazyArray(a1,  Add(), (3.0,));

julia> <strong i="17">@code_native</strong> a2[1]
    .text
Filename: none
Source line: 1
    push    RBP
    mov RBP, RSP
Source line: 1
    mov RAX, QWORD PTR [RDI + 8]
    mov RCX, QWORD PTR [RAX + 8]
    lea RDX, QWORD PTR [RSI - 1]
    cmp RDX, QWORD PTR [RCX + 16]
    jae L64
    mov RCX, QWORD PTR [RCX + 8]
    movsd   XMM0, QWORD PTR [RCX + 8*RSI - 8]
    mov RAX, QWORD PTR [RAX + 24]
    mov RAX, QWORD PTR [RAX + 16]
    divsd   XMM0, QWORD PTR [RAX + 8]
    mov RAX, QWORD PTR [RDI + 24]
    mov RAX, QWORD PTR [RAX + 16]
    addsd   XMM0, QWORD PTR [RAX + 8]
    pop RBP
    ret
L64:    movabs  RAX, jl_bounds_exception
    mov RDI, QWORD PTR [RAX]
    movabs  RAX, jl_throw_with_superfluous_argument
    mov ESI, 1
    call    RAX

与等价物相反:

julia> fun(x) = x/2.0 + 3.0
fun (generic function with 1 method)

julia> <strong i="21">@code_native</strong> fun(a1[1])
    .text
Filename: none
Source line: 1
    push    RBP
    mov RBP, RSP
    movabs  RAX, 139856006157040
Source line: 1
    mulsd   XMM0, QWORD PTR [RAX]
    movabs  RAX, 139856006157048
    addsd   XMM0, QWORD PTR [RAX]
    pop RBP
    ret

jae L64之前的部分是数组边界检查。 使用@inbounds可能会有所帮助(如果合适)。

下面的部分,其中两行以mov RAX, ...开头,是双重间接,即访问指向指针(或数组数组,或指向数组的指针等)的指针。 这可能与 LazyArray 的内部表示有关——也许使用不可变对象(或 Julia 以不同方式表示不可变对象)可能会有所帮助。

无论如何,代码仍然相当快。 为了让它更快,它需要被内联到调用者中,从而提供进一步的优化机会。 如果你从一个循环中调用这个表达式会发生什么?

另外:如果你不是从 REPL 而是从一个函数中反汇编它会发生什么?

也忍不住注意到第一个版本执行了一个实际的
除法,而第二个已将 x/2 转换为乘法。

感谢您的评论。

@eschnett LazyArray已经是不可变的,我在循环中使用@inbounds 。 在https://gist.github.com/nalimilan/d345e1c080984ed4c89a运行要点后,您可以通过以下方式检查循环中给出的内容:

function test_lazy!(newa, a)
    a1 = LazyArray(a, Divide(), (2.0,))
    a2 = LazyArray(a1, Add(), (3.0,))
    collect!(newa, a2)
    newa
end
<strong i="11">@code_native</strong> test_lazy!(newa, a); 

所以也许我需要的只是能够强制内联? 在我的尝试中,将@inline添加到getindex不会改变时间。

@toivoh什么可以解释在后一种情况下划分没有简化?

我继续尝试使用两个参数的版本(称为LazyArray2 )。 结果是像x .+ y这样的简单操作,实际上使用LazyArray2比当前的.+更快,而且它也非常接近显式循环(这些是 1000 次调用,见 https://gist.github.com/nalimilan/d345e1c080984ed4c89a):

# With LazyArray2, filling existing array
elapsed time: 0.028212517 seconds (56000 bytes allocated)

# With explicit loop, filling existing array
elapsed time: 0.013500379 seconds (0 bytes allocated)

# With LazyArray2, allocating a new array before filling it
elapsed time: 0.098324278 seconds (80104000 bytes allocated, 74.16% gc time)

# Using .+ (thus allocating a new array)
elapsed time: 0.078337337 seconds (80712000 bytes allocated, 52.46% gc time)

所以看起来这种策略可以替代所有元素操作,包括.+.*等运算符。

实现常见的操作看起来也很有竞争力,比如计算沿矩阵维度的平方差之和,即sum((x .- y).^2, 1) (再次参见要点):

# With LazyArray2 and LazyArray (no array allocated except the result)
elapsed time: 0.022895754 seconds (1272000 bytes allocated)

# With explicit loop (no array allocated except the result)
elapsed time: 0.020376307 seconds (896000 bytes allocated)

# With element-wise operators (temporary copies allocated)
elapsed time: 0.331359085 seconds (160872000 bytes allocated, 50.20% gc time)

@nalimilan
您使用 LazyArrays 的方法似乎类似于蒸汽融合工作 Haskell [1, 2] 的方式。 也许我们可以应用那个领域的想法?

[1] http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.104.7401
[2] http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.421.8551

@vchuravy谢谢。 这确实相似,但更复杂,因为 Julia 使用命令式模型。 相反,在 Haskell 中,编译器需要处理多种情况,甚至需要处理 SIMD 问题(稍后在 Julia 中由 LLVM 处理)。 但老实说,我无法解析这些论文中的所有内容。

@nalimilan我知道这种感觉。 我发现第二篇论文特别有趣,因为它讨论了广义流融合,这显然允许在向量上建立一个很好的计算模型。

我认为我们应该从中得到的要点是,像mapreduce这样的结构与惰性相结合可以足够快(甚至比显式循环更快)。

据我所知,大括号在调用语法中仍然可用。 如果这变成func{x}怎么办? 也许有点太浪费了?

关于快速矢量化(在 SIMD 的意义上)的主题,我们有什么方法可以模仿 Eigen 的做法吗?

这是一个建议,用我上面所说的LazyArrayLazyArray2的概括来替换所有当前的元素操作。 这当然依赖于我们可以在不依赖 NumericFuns.jl 中的仿函数的情况下快速处理所有函数的假设。

1) 添加新语法f.(x)f$(x)或任何会在 $ x #$ 的每个元素上创建LazyArray调用f()的语法。

2)按照broadcast当前的工作方式概括此语法,以便例如f.(x, y, ...)f$(x, y, ...)创建LazyArray ,但扩展x的单例维度y , ... 以便给它们一个共同的大小。 这当然可以通过对索引的计算即时完成,这样扩展的数组就不会被实际分配。

3) 使.+ , .- , .* , ./ , .^等使用LazyArray代替broadcast

4) 引入一个新的赋值运算符.=$=它将转换(调用collect )一个LazyArray到一个真正的数组(类型取决于它的通过提升规则输入,并且元素类型取决于输入的元素类型和调用的函数)。

5) 甚至可以将broadcast替换为对LazyArray的调用,并将结果立即collect离子转换为真实数组。

第 4 点是关键:逐元素操作永远不会返回真正的数组,总是LazyArray s,因此当组合多个操作时,不会制作副本,并且可以融合循环以提高效率。 这允许在不分配临时变量的情况下对结果调用sum之类的缩减。 因此,对于密集数组和稀疏矩阵,这种表达式将是惯用且有效的:

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

我认为返回这种轻量级对象非常适合数组视图和Transpose / CTranspose的新图片。 这意味着在 Julia 中,您可以使用密集且可读的语法非常有效地执行复杂的操作,但在某些情况下,当您需要独立于“伪数组”时,您必须显式调用copy它所基于的真实数组。

这听起来确实是一个重要的功能。 元素运算符的当前行为对于新用户来说是一个陷阱,因为语法很好而且很短,但性能通常非常糟糕,显然比在 Matlab 中更差。 就在上周,julia-users 上的几个线程出现了性能问题,这些问题会随着这样的设计而消失:
https://groups.google.com/d/msg/julia-users/t0KvvESb9fA/6_ZAp2ujLpMJ
https://groups.google.com/d/msg/julia-users/DL8ZsK6vLjw/w19Zf1lVmHMJ
https://groups.google.com/d/msg/julia-users/YGmDUZGOGgo/LmsorgEfXHgJ

出于这个问题的目的,我将语法与懒惰分开。 不过你的提议很有趣。

似乎到了只有_这么多点_的地步。 特别是中间的例子最好写成

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

它只需要基本功能和高效的map ( .|> )。

这是一个有趣的比较:

y .=  √π exp.(-x .^ 2) .* sin.(k .* x) .+ im * log.(x .- 1)
y =  [√π exp(-x[i]^ 2) .* sin(k * x[i]) .+ im * log(x[i] - 1) for i = 1:length(x)]

如果将for ...部分打折,则推导式只长一个字符。 我几乎宁愿有一个缩写的理解语法而不是所有这些点。

一维推导不会保留形状,但现在我们有for i in eachindex(x)也可以改变。

推导式的一个问题是它们不支持 DataArrays。

我认为看看 .Net 上发生的一大堆看起来与 LazyArray 想法非常相似的事情可能是值得的。 从本质上讲,它在我看来非常接近 LINQ 风格的方法,其中您的语法看起来像我们现在拥有的元素,但实际上该语法构建了一个表达式树,然后该表达式树随后以某种有效的方式进行评估. 那是不是很接近?

在 .Net 上,他们采用了这个想法:您可以在多个 CPU 上并行执行这些表达式树(通过添加 .AsParallel()),或者您可以使用 DryadLINQ 在大型集群上运行它们,甚至可以在http:/ /research.microsoft.com/en-us/projects/accelerator/ (后者可能没有与 LINQ 完全集成,但如果我没记错的话,它在精神上很接近),或者如果它当然可以翻译成 SQL数据是那种形状,您只使用可以转换为 SQL 语句的运算符。

我的感觉是 Blaze 也在朝着这个方向发展,即一种轻松构建描述计算的对象的方法,然后您可以为它使用不同的执行引擎。

不确定这是否很清楚,但在我看来,整个问题应该在如何生成低级高效 SIMD 类代码以及如何将其用于 GPU 计算、集群、并行计算等

是的,你是对的,较长的例子有太多的点。 但是两个较短的更典型,在这种情况下,语法很短很重要。 我想将语法与懒惰区分开来,但正如您的评论所示,这似乎很难,我们总是将两者混为一谈!

可以想象调整理解语法,例如y = [sqrt(x + 2) over x] 。 但正如@johnmyleswhite指出的那样,它们应该支持DataArrays ,但也应该支持稀疏矩阵和任何新的数组类型。 所以这又是一个混合语法和特性的例子。

更根本的是,我认为我的提案提供的替代方案的两个功能是:
1) 支持使用y[:] = sqrt.(x .+ 2)进行无分配就地分配。
2) 支持像sum((x .- y).^2, 1)这样的无分配减少。

是否可以提供其他解决方案(忽略语法问题)?

@davidanthoff谢谢,现在看看(我认为LazyArray也可以支持并行计算)。

也许这可以与生成器结合使用——它们也是一种惰性数组。 我有点喜欢[f(x) over x]理解语法,尽管它在概念上对于新手来说可能很困难(因为相同的名称有效地用于元素和数组本身)。 如果没有括号的理解会创建一个生成器(就像我很久以前玩过的那样),那么使用这些新的不带括号的 over-x-style-comprehensions 来返回一个 LazyArray 而不是立即收集它是很自然的。

@mbauman是的,生成器和惰性数组共享许多属性。 使用括号来收集生成器/惰性数组,而不是添加它们来保持惰性对象的想法听起来很酷。 所以关于我上面的例子,一个人可以同时写 1) y[:] = sqrt(x + 2) over xsum((x - y)^2 over (x, y), 1) (虽然我觉得即使是新手也很自然,让我们把over的问题留给自行车脱落会议并首先关注基本面)。

我喜欢f(x) over x的想法。 我们甚至可以使用f(x) for x来避免使用新关键字。 事实上[f(x) for x=x]已经有效。 然后,我们需要使推导等效于map ,因此它们可以适用于非数组。 Array只是默认值。

我必须承认我也开始喜欢over的想法。 over作为 map 和for在列表理解中的一个区别是在多个迭代器的情况下会发生什么: [f(x, y) for x=x, y=y]产生一个矩阵。 对于地图情况,您通常仍然需要一个向量,即[f(x, y) over x, y]将等同于[f(x, y) for (x,y) = zip(x, y)]] 。 正因为如此,我仍然认为引入一个额外的关键字over是值得的,因为正如这个问题所提出的,在多个向量上的map ing 非常常见并且需要简洁。

嘿,我让 Jeff 相信了语法! ;-)

这属于 #4470 旁边,所以现在添加到 0.4-projects。

如果我理解讨论的要点,主要问题是我们想要获得类似映射的语法:

  • 适用于各种数据类型,例如 DataArrays,不仅是原生数组;
  • 与手动编写的循环一样快。

使用内联可能可以做到这一点,但要非常小心以确保内联有效。

不同的方法呢:根据推断的数据类型使用宏。 如果我们可以推断出数据结构是DataArray,我们使用DataArrays库提供的map-macro。 如果是 SomeKindOfStream,我们使用提供的流库。 如果我们无法推断类型,我们只需使用动态调度的通用实现。

这可能会迫使数据结构创建者编写这样的宏,但只有当它的作者希望它具有真正有效的执行时才需要它。

如果我写的不清楚,我的意思是像[EXPR for i in collection if COND]这样的东西可以翻译成eval(collection_mapfilter_macro(:(i), :(EXPR), :(COND))) ,其中collection_mapfilter_macro是根据推断的集合类型选择的。

不,我们不想做那样的事情。 如果 DataArray 定义了map (或等效项),则无论可以推断出什么,都应始终为 DataArrays 调用其定义。

这个问题实际上不是关于实现,而是关于语法。 现在很多人习惯于sin(x)隐式映射,如果x是一个数组,但是这种方法存在很多问题。 问题是什么替代语法是可以接受的。

1) 支持使用y[:] = sqrt.(x .+ 2)的无分配就地分配
2) 支持无分配减少,如sum((x .- y).^2, 1)

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

看看其他人的这三个例子,我认为使用for语法最终会是这样的:
1) y[:] = [ sqrt(x + 2) for x ])
2) sum([ (x-y)^2 for x,y ], 1)

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

我非常喜欢这个! 它创建一个临时数组的事实非常明确,并且仍然可读且简洁。

不过,小问题, x[:] = [ ... for x ]是否可以在不分配临时数组的情况下改变数组?
我不确定这是否会带来很多好处,但我可以想象它会对大型阵列有所帮助。
我可以相信,这可能是完全不同的一锅鱼,应该在其他地方讨论。

@Mike43110您的x[:] = [ ... for x ]可以写成x[:] = (... for x) ,RHS 创建一个生成器,它将逐个元素收集以填充x ,而不分配副本。 这就是我上面的LazyArray实验背后的想法。

如果将 [f <- y] 语法与Int[f <- y]语法结合用于知道其输出类型并且不需要从f(y[1])插入其他元素的映射,那么[f <- y]语法会很好。

特别是,因为这也为mapslices提供了一个直观的界面,所以[f <- rows(A)]其中rows(A) (或columns(A)slices(A, dims) )返回一个Slice对象,因此可以使用分派:

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

当您添加索引时,这会变得有点困难。 例如

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

很难与它的简洁性相提并论。 理解风格的爆炸非常糟糕:

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

更糟糕的是,需要聪明地知道这是嵌套迭代的情况,并且不能用单个over来完成。 虽然如果fg有点贵,这可能会更快:

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

但更长。

这种例子似乎支持“点”,因为这可能会给f.(x[:,j]) .* g.(x[i,:])

@JeffBezanson我不确定您评论的意图是什么。 有没有人建议摆脱.*语法?

不; 我在这里关注fg 。 这是一个示例,您不能只在行尾添加over x

好的,我明白了,我错过了评论的结尾。 在这种情况下,点版本确实更好。

虽然使用数组视图,但会有一个相当有效(AFAICT)且不那么丑陋的替代方案:
[ f(y) * g(z) for y in x[:,j], z in x[i,:] ]

上面的例子可以通过嵌套关键字来解决吗?

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

被解释为

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

然而

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

变成

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

然后,上面的具体示例将类似于

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

编辑:回想起来,这并不像我想象的那么简洁。

@JeffBezanson怎么样

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

相当于f.(x[:,n] .* g.(x[m,:]) 。 新语法x[:i,n]意味着i作为容器索引的迭代器在本地引入x[:,n] 。 我不知道这是否有可能实现。 但看起来(表面上)既不丑也不麻烦,语法本身为迭代器提供了界限,即 1:length(x[:,n])。 就关键字而言,“over”可以表示要使用整个范围,而如果用户希望指定 1:length(x[:,n]) 的子范围,则可以使用“for”:

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

@davidagold:i已经表示符号i

啊,是的,好点。 好吧,只要点是公平的游戏呢?

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

其中,点表示i作为迭代器在本地引入,超过 1:length(x[:,n)。 我想本质上这会将点符号从修改函数切换到修改数组,或者更确切地说是它们的索引。 这将从“点蠕变”中拯救一个杰夫指出:

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

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

虽然我认为后者稍微短一些。 [编辑:另外,如果在没有歧义的情况下可以省略over i ,那么实际上语法稍微短一些:

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

理解语法的一个潜在优势是它可以允许更广泛的元素操作模式。 例如,如果解析器理解在x[m, .i]中使用i进行索引是隐式模长度(x[m,:]),那么可以这样写

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

以相反的顺序将x的元素与y的元素相乘,即x的第一个元素与y的最后一个元素等相乘. 可以写

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

将 $#$ x $#$ 的第i yi+1元素(其中x的最后一个元素将相乘y的第一个元素,因为在此上下文中将索引理解为模长度(x))。 如果p::Permutation置换 (1, ..., length(x)) 可以写

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

将 x 的第i y x p(i)元素。

无论如何,这只是一个局外人对一个完全投机问题的拙见。 =p 我很感激有人花时间考虑它。

将使用 r 样式回收的增强版 vectorize 可能非常有用。 也就是说,与最大参数的大小不匹配的参数通过回收进行扩展。 然后用户可以轻松地矢量化他们想要的任何东西,而不管参数的数量等。

unvectorized_sum(a, b, c, d) = a + b + c + d
vectorized_sum = @super_vectorize(unvectorized_sum)

a = [1, 2, 3, 4]
b = [1, 2, 3]
c = [1, 2]
d = 1

A = [1, 2, 3, 4]
B = [1, 2, 3, 1]
C = [1, 2, 1, 2]
D = [1, 1, 1, 1]

vectorized_sum(a, b, c, d) = vectorized_sum(A, B, C, D) = [4, 7, 8, 8]

我倾向于认为回收利用太多的安全性来换取便利。 通过回收,很容易编写有缺陷的代码来执行而不会引发任何错误。

当我第一次读到 R 的这种行为时,我立刻想知道为什么有人会认为这是一个好主意。 在大小不匹配的数组上隐含地做这件事是一件非常奇怪和令人惊讶的事情。 在少数情况下,您可能希望扩展较小的数组,但同样您可能希望零填充或重复结束元素,或外推,或错误,或任何数量的其他应用程序相关选择。

是否使用@super_vectorize将掌握在用户手中。 也可以针对各种情况发出警告。 例如,在 R 中,

c(1, 2, 3) + c(1, 2)
[1] 2 4 4
Warning message:
In c(1, 2, 3) + c(1, 2) :
  longer object length is not a multiple of shorter object length

我不反对让它成为用户可以选择是否使用的可选事物,但是当它可以在包中完成时,它不需要用基础语言实现。

@vectorize_1arg@vectorize_2arg都已包含在 Base 中,它们为用户提供的选项似乎有些有限。

但是这个问题的重点是设计一个从 Base 中删除@vectorize_1arg@vectorize_2arg的系统。 我们的目标是从语言中删除向量化函数,并用更好的抽象替换它们。

例如,回收可以写成

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

这对我来说非常接近理想的写作方式。 没有人需要为您构建它。 主要问题是(1)这是否可以更简洁,(2)如何将其扩展到其他容器类型。

查看@davidagold的建议,我想知道var:是否不能用于变量将是冒号前的名称的那种事情。 我看到这个语法曾经意味着A[var:end]所以它似乎是可用的。

然后f(x[:,j]) .* g(x[i,:])将是f(x[a:,j]) * g(x[i,b:]) for a, b这并不太糟糕。

不过,多个冒号会有点奇怪。

f(x[:,:,j]) .* g(x[i,:,:]) -> f(x[a:,a:,j]) * g(x[i,b:,b:]) for a, b是我最初的想法。

好的,这是一个关于回收计划的简短说明。 它应该能够处理 n 维数组。 可能有可能将元组类似地合并到向量中。

using DataFrames

a = [1, 2, 3]
b = 1
c = [1 2]
d = <strong i="6">@data</strong> [NA, 2, 3]

# coerce an array to a certain size using recycling
coerce_to_size = function(argument, dimension_extents...)

  # number of repmats needed, initialized to 1
  dimension_ratios = [dimension_extents...]

  for dimension in 1:ndims(argument)

    dimension_ratios[dimension] = 
      ceil(dimension_extents[dimension] / size(argument, dimension))
  end

  # repmat array to at least desired size
  if typeof(argument) <: AbstractArray
    rep_to_size = repmat(argument, dimension_ratios...)
  else
    rep_to_size = 
      fill(argument, dimension_ratios...)
  end

  # cut down array to exactly desired size
  dimension_ranges = [1:i for i in dimension_extents]
  dimension_ranges = tuple(dimension_ranges...)

  rep_to_size = getindex(rep_to_size, dimension_ranges...)  

end

recycle = function(argument_list...)

  # largest dimension in arguments
  max_dimension = maximum([ndims(i) for i in argument_list])
  # initialize dimension extents to 1
  dimension_extents = [1 for i in 1:max_dimension]

  # loop through argument and dimension
  for argument_index in 1:length(argument_list)
    for dimension in 1:ndims(argument_list[argument_index])
      # find the largest size for each dimension
      dimension_extents[dimension] = maximum([
        size(argument_list[argument_index], dimension),
        dimension_extents[dimension]
      ])
    end
  end

  expand_arguments = 
    [coerce_to_size(argument, dimension_extents...) 
     for argument in argument_list]
end

recycle(a, b, c, d)

mapply = function(FUN, argument_list...)
  argument_list = recycle(argument_list...)
  FUN(argument_list...)
end

mapply(+, a, b, c, d)

显然,这不是最优雅或最快速的代码(我是最近的 R 移民)。 我不知道如何从这里到@vectorize宏。

编辑:组合冗余循环
编辑2:分离出强制大小。 目前仅适用于 0-2 维度。
编辑 3: 这样做的一种稍微优雅的方法是定义一种特殊类型的带有 mod 索引的数组。 那是,

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

编辑 4:我想知道的事情存在,因为这很难写:hcat 和 vcat 的 n 维概括? 一种用每个特定位置的索引的列表或元组填充 n 维数组(匹配给定数组的大小)的方法? repmat 的 n 维泛化?

[pao:语法高亮]

您真的不想在 Julia 中使用foo = function(x,y,z) ... end语法定义函数,尽管它确实有效。 这会创建名称到匿名函数的非常量绑定。 在 Julia 中,规范是使用泛型函数,并且函数的绑定是自动恒定的。 否则你会得到糟糕的表现。

我不明白为什么 repmat 在这里是必要的。 用每个位置的索引填充的数组也是一个警告信号:不应该使用大量内存来表示这么少的信息。 我相信这样的技术真的只在一切都需要“矢量化”的语言中才有用。 在我看来,正确的方法是运行一个循环来转换一些索引,如https://github.com/JuliaLang/julia/issues/8450#issuecomment -111898906。

是的,这是有道理的。 这是一个开始,但我无法弄清楚如何在最后进行循环,然后制作一个@vectorize宏。

function non_zero_mod(big::Number, little::Number)
  result = big % little
  result == 0 ? little : result
end

function mod_select(array, index...)
  # just return singletons
  if !(typeof(array) <: AbstractArray) return array end
  # find a new index with moded values
  transformed_index = 
      [non_zero_mod( index[i], size(array, i) )
       for i in 1:ndims(array)]
  # return value at moded index
  array[transformed_index...]
end

function mod_value_list(argument_list, index...)
  [mod_select(argument, index...) for argument in argument_list]
end

mapply = function(FUN, argument_list...)

  # largest dimension in arguments
  max_dimension = maximum([ndims(i) for i in argument_list])
  # initialize dimension extents to 1
  dimension_extents = [1 for i in 1:max_dimension]

  # loop through argument and dimension
  for argument_index in 1:length(argument_list)
    for dimension in 1:ndims(argument_list[argument_index])
      # find the largest size for each dimension
      dimension_extents[dimension] = maximum([
        size(argument_list[argument_index], dimension),
        dimension_extents[dimension]
      ])
    end
  end

  # more needed here
  # apply function over arguments using mod_value_list on arguments at each position
end

在演讲中, @JeffBezanson提到了语法sin(x) over x ,为什么不更像:
sin(over x) ? (或使用某些字符而不是over作为关键字)

一旦解决了这个问题,我们也可以解决 #11872

我希望我不会迟到,但我只想为@davidagold的语法建议提供+1。 它在概念上清晰,简洁,写起来感觉很自然。 我不确定.是否是最好的识别字符,或者实际实现的可行性如何,但可以使用宏来进行概念验证(基本上就像@devec ,但可能更容易实现)。

它还具有“适应”现有数组理解语法的好处:

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

对比

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

两者之间的主要区别在于前者对形状保存有更多限制,因为它暗示了一张地图。

overrangewindow在 OLAP 空间中有一些现有技术作为迭代的修饰符,这似乎是一致的。

我不喜欢.语法,因为这似乎是对线路噪音的一种爬行。

$ 可能是一致的,实习生将 i,j 迭代到表达式中的值?

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

对于表达式的自动向量化,我们可以不taint表达式中的一个向量并让类型系统将表达式提升到向量空间吗?

我与时间序列操作类似,其中 Julia 的表现力已经允许我编写

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

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

观察时输出向量的时间序列。

缺少的主要部分是将现有功能自动提升到空间中的能力。 这必须手动完成

本质上,我希望能够定义如下内容......

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

然后能够专攻特定的操作

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

@JeffBezanson

如果我理解正确,我想为您的 JuliaCon 2015 评论提出一个解决方案,以解决上述评论:
“[...] 告诉库编写者将@vectorize放在所有适当的函数上是愚蠢的;你应该能够只编写一个函数,如果有人想为他们使用 map 的每个元素计算它。”
(但我不会解决另一个基本问题“[..] 没有真正令人信服的理由说明 sin、exp 等应该隐式映射到数组上。”)

在 Julia v0.40 中,我已经能够得到一个比@vectrorize更好的解决方案(在我看来):

abstract Vectorizable{Fn}
#Could easily have added extra argument to Vectorizable, but want to show inheritance case:
abstract Vectorizable2Arg{Fn} <: Vectorizable{Fn}

call{F}(::Type{Vectorizable2Arg{F}}, x1, x2) = eval(:($F($x1,$x2)))
function call{F,T1,T2}(fn::Type{Vectorizable2Arg{F}}, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

#Function in need of vectorizing:
function _myadd(x::Number, y::Number)
    return x+y+1
end

#"Register" the function as a Vectorizable 2-argument (alternative to @vectorize):
typealias myadd Vectorizable2Arg{:_myadd}

<strong i="13">@show</strong> myadd(5,6)
<strong i="14">@show</strong> myadd(collect(1:10),collect(21:30.0)) #Type stable!

这或多或少是合理的,但有点类似于@vectorize解决方案。 为了使向量化更加优雅,我建议 Julia 支持以下内容:

abstract Vectorizable <: Function
abstract Vectorizable2Arg <: Vectorizable

function call{T1,T2}(fn::Vectorizable2Arg, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

#Note: by default, functions would normally be <: Function:
function myadd(x::Number, y::Number) <: Vectorizable2Arg
    return x+y+1
end

而已! 从 Vectorizable 函数继承的函数将使其可向量化。

我希望这与您正在寻找的内容一致。

问候,

在没有多重继承的情况下,函数如何从Vectorizable和其他东西继承? 您如何将特定方法的继承信息与泛型函数的继承信息联系起来?

@ma-laforge 你已经可以这样做了——定义一个类型myadd <: Vectorizable2Arg ,然后在 Number 上为myadd实现call Number

感谢@JeffBezanson!

事实上,我的解决方案几乎可以和我想要的一样好:

abstract Vectorizable
#Could easily have parameterized Vectorizable, but want to show inheritance case:
abstract Vectorizable2Arg <: Vectorizable

function call{T1,T2}(fn::Vectorizable2Arg, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

#SECTION F: Function in need of vectorizing:
immutable MyAddType <: Vectorizable2Arg; end
const myadd = MyAddType()
function call(::MyAddType, x::Number, y::Number)
    return x+y+1
end

<strong i="7">@show</strong> myadd(5,6)
<strong i="8">@show</strong> myadd(collect(1:10),collect(21:30.0)) #Type stable

现在,唯一缺少的是一种对任何函数进行“子类型化”的方法,以便可以用更优雅的语法替换整个 F 部分:

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

注意:我将类型“MyAddType”和函数名设为单例对象“myadd”,因为我发现生成的语法比在调用签名中使用Type{Vectorizable2Arg}更好:

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

可悲的是,根据您的回复,听起来这_not_不是@vectorize宏的“愚蠢”的充分解决方案。

问候,

@johnmyleswhite

我想回复你的评论,但我想我不明白。 你能澄清一下吗?

我_可以_说一件事:
“可矢量化”没有什么特别之处。 这个想法是任何人都可以定义自己的函数“类”(例如: MyFunctionGroupA<:Function )。 然后,他们可以通过定义自己的“调用”签名(如上所示)来捕获对该类型函数的调用。

话虽如此:我的建议是 Base 中定义的函数应该使用Base.Vectorizable <: Function (或类似的东西),以便自动生成矢量化算法。

然后,我建议模块开发人员使用类似于以下的模式来实现自己的功能:

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

当然,他们必须提供自己的promote_type(::Type{MyType},::Type{MyType})版本 - 如果默认值尚未返回MyType

如果默认的向量化算法不充分,则没有什么可以阻止用户实现自己的层次结构:

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

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

@ma-laforge,抱歉不清楚。 我担心的是任何层次结构总是会缺少重要信息,因为 Julia 具有单一继承,这要求您为每个函数提交一个父类型。 如果您使用像myfunction(x::MyType, y::MyType) <: Base.Vectorizable这样的东西,那么您的函数将不会受益于其他人定义像Base.NullableLiftable这样的自动生成可空函数的概念。

看起来这不会是特征的问题(参见 https://github.com/JuliaLang/julia/pull/13222)。 同样相关的是声明方法为纯的新可能性(https://github.com/JuliaLang/julia/pull/13555),这可能自动暗示这种方法是可向量化的(至少对于单参数方法)。

@johnmyleswhite

如果我理解正确:我认为这对于 _this_ 案例来说不是问题。 那是因为我提出了一种设计模式。 你的函数_have_没有继承自Base.Vectorizable ...你可以使用你自己的。

我对NullableLiftables真的不太了解(我的 Julia 版本中似乎没有这个)。 但是,假设它继承自Base.Function (这在我的 Julia 版本中也是不可能的):

NullableLiftable <: Function

然后,您的模块可以实现(仅一次)一个 _new_ 可矢量化子类型:

abstract VectorizableNullableLiftable <: NullableLiftable

function call{T1,T2}(fn::VectorizableNullableLiftable, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

因此,从现在开始,任何定义函数<: VectorizableNullableLiftable的人都会自动应用您的矢量化代码!

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

我确实明白,拥有多个 Vectorizable-type 仍然有点痛苦(而且有点不雅)......但至少它会消除 Julia 中令人讨厌的重复(1)之一(必须注册 _each_ 新添加函数调用@vectorize_Xarg)。

(1)假设 Julia 支持函数继承(例如: myfunction(...)<: Vectorizable ) - 在 v0.4.0 上它似乎不支持。 我在 Julia 0.4.0 中工作的解决方案只是一个 hack……你仍然需要注册你的函数……并不比调用 @vectorize_Xarg 好多少

我仍然认为这是一种错误的抽象。 可以或应该“矢量化”的函数不是特定类型的函数。 _Every_ 函数可以传递给map ,赋予它这种行为。

顺便说一句,通过我在 jb/functions 分支中进行的更改,您将能够执行function f(x) <: T (但显然,仅适用于f的第一个定义)。

好的,我想我更好地理解你在寻找什么......这不是我建议的。 我认为这也可能是@johnmyleswhite对我的建议提出的问题的一部分......

...但是如果我现在了解问题所在,那么解决方案对我来说似乎更简单:

function call{T1,T2}(fn::Function, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

myadd(x::Number, y::Number) = x+y+1

由于myadd的类型是Function ,它应该被call函数捕获......它会这样做:

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

但是call不会自动调度函数,出于某种原因(不知道为什么):

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

但我想这种行为应该不会太难改变。 就个人而言,我不知道我对制作这种包罗万象的包罗万象的功能有何感想,但听起来这就是你想要的。

我注意到一些奇怪的事情:如果没有输入,Julia 已经自动矢量化了函数:

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

回复:顺便说一句...:
凉爽的! 我想知道通过子类型化函数我能做些什么巧妙的事情:)。

如果没有输入,Julia 已经自动矢量化了函数

之所以出现这种情况,是因为函数内部使用的+运算符是矢量化的。

但我想这种行为应该不会太难改变。 就个人而言,我不知道我对制作这种包罗万象的包罗万象的功能有何感想,但听起来这就是你想要的。

我分享你的预订。 您不能明智地定义“这是如何调用任何函数”的定义,因为每个函数本身都说明了在调用它时要做什么——这就是函数!

我应该说:...用户定义的非 unicode 中缀运算符,因为我认为我们不想要求用户输入 unicode 字符来访问这样的核心功能。 虽然我看到 $ 实际上是添加的其中之一,所以谢谢你! 哇,所以这实际上在今天的 Julia 中有效(即使它还不是“快”……):

朱莉娅>($)=地图
朱莉娅>罪$(0.5 *(abs2 $(xy)))

@binarybana / \mapsto怎么样?

julia> x, y = rand(3), rand(3);

julia> ↦ = map    # \mapsto<TAB>
map (generic function with 39 methods)

julia> sin ↦ (0.5 * (abs2 ↦ (x-y)))
3-element Array{Float64,1}:
 0.271196
 0.0927406
 0.0632608

还有:

FWIW,我至少最初假设\mapsto是 lambdas 的替代语法,因为它通常用于数学中,本质上(在其 ASCII 化身中, -> )在 Julia 中也是. 我认为这会相当混乱。

说到数学……在模型理论中,我已经看到map通过将函数应用于不带括号的元组来表示。 也就是说,如果\bar{a}=(a_1, \dots, a_n) ,则f(\bar{a})f(a_1, \dots, a_n) (即,本质上是apply )并且f\bar{a}(f(a_1), \dots, f(a_n)) (即map )。 用于定义同态等的有用语法,但并非所有这些都可以轻松转移到编程语言:-}

\Mapsto这样的其他替代品怎么样,你会把它和=> (对)混淆吗? 我认为这两个符号在这里可以并排区分:

  • ->

有很多看起来相似的符号,如果我们只使用看起来非常不同或纯 ASCII 的符号,为什么会有这么多符号呢?

我认为这会相当混乱。

我认为文档和经验可以解决这个问题,你同意吗?

还有很多其他箭头之类的符号,老实说,我不知道它们在数学中是做什么用的,否则我只提出这个是因为它们的名字中有map ! :微笑:

我想我的意思是->是 Julia 尝试用 ASCII 表示的尝试。 因此,使用来表示其他东西似乎是不明智的。 并不是我无法从视觉上区分它们:-)

我的直觉是,如果我们要使用成熟的数学符号,我们可能至少要考虑一下 Julia 的用法与既定用法有何不同。 选择名称中带有map的符号似乎是合乎逻辑的,但在这种情况下,它指的是地图的定义(或不​​同类型的地图,或此类地图的类型)。 在 Pair 中的用法也是如此,或多或少,不是通过定义参数映射到什么来定义完整的函数,而是实际上列出给定参数(参数值)映射到什么 - 即,它是显式的元素概念上的枚举函数(例如,字典)。

@Ismael-VC 您的建议的问题是您需要调用map两次,而理想的解决方案将不涉及临时数组并减少到map((a, b) -> sin(0.5 * abs2(a-b)), x, y) 。 此外,重复两次在视觉和打字方面都不是很好(如果有 ASCII 等价物就好了)。

R 用户可能不喜欢这个想法,但是如果我们倾向于弃用当前的~中缀宏特殊情况解析(像 GLM 和 DataFrames 之类的包需要更改为宏解析他们的公式 DSL,参考 https:/ /github.com/JuliaStats/GLM.jl/issues/116),这将释放中缀 ascii 运算符的稀有商品。

a ~ b可以在 base 中定义为map(a, b) ,也许a .~ b可以定义为broadcast(a, b) ? 如果它被解析为传统的中缀运算符,那么宏 DSL 就像 R 公式接口的仿真一样,可以自由地在宏内部实现自己对运算符的解释,就像 JuMP 对<===所做的那样.

这可能不是最漂亮的建议,但如果你过度使用它们,Mathematica 中的速记也不是......我最喜欢的是.#&/@

:+1:为了减少特殊的大小写和更多的通用性和一致性,您为~.~提出的含义对我来说看起来很棒。

+1托尼。

@tkelman要清楚,您将如何以完全矢量化的方式编写例如sin(0.5 * abs2(a-b))

大概理解一下吧。 我不认为中缀映射适用于可变参数或就地,因此自由语法的可能性并不能解决所有问题。

所以这并不能真正解决这个问题。 :-/

到目前为止, sin(0.5 * abs2(a-b)) over (a, b)语法(或变体,可能使用中缀运算符)是最吸引人的。

本期的标题是“ map(func, x)的替代语法”。 使用中缀运算符并不能解决映射/循环融合以消除临时性,但我认为这可能是比语法更广泛、相关但技术上独立的问题。

是的,我同意@tkelman ,关键是要为map提供替代语法,这就是为什么我建议使用\mapsto的原因。 @nalimilan提到的似乎更广泛,更适合另一个问题恕我直言How to fully vecotrize expressions

<rambling>
这个问题已经持续了一年多(并且可能会像现在许多其他问题一样无限期地继续下去)! 但是我们现在可以有Alternative syntax for map(func, x) 。 在 ±450 位朱利安贡献者中,只有 41 位能够找到这个问题和/或愿意分享意见(这对于 github 问题来说很多,但在这种情况下显然还不够),总而言之,没有太多不同的建议(这不仅仅是同一概念的微小变化)。

我知道你们中的一些人不喜欢这个想法或认为进行调查/民意调查的价值(:shocked:),但由于我不需要为这样的事情征求任何人的许可,我还是会这样做。 我们没有充分利用我们的社区和社交网络以及其他社区的方式有点可悲,我们没有看到它的价值更可悲,让我们看看我是否可以收集更多不同新鲜的意见,或者至少检查找出大多数人对这个特定问题的当前观点的看法,作为一个实验,看看它是如何进行的。 也许它确实没用,也许不是,只有一种方法可以真正知道。
</rambling>

@Ismael-VC:如果您真的想进行民意调查,您必须做的第一件事就是仔细考虑您要问的问题。 您不能期望每个人都通读整个线程并总结已单独讨论的选项。

map(func, x)还涵盖了诸如map(v -> sin(0.5 * abs2(v)), x)之类的内容,这就是该线程所讨论的内容。 让我们不要把它移到另一个线程,因为它会使记住上面讨论的所有建议变得更加困难。

我不反对为使用map应用泛型函数的简单情况添加语法,但如果我们这样做,我认为同时考虑更广泛的情况是个好主意。 如果不是因为这个,这个问题早就可以解决了。

@Ismael-VC 民意调查不太可能在这里提供帮助,恕我直言。 我们并不是要找出几个解决方案中的哪一个是最好的,而是要找到一个没有人真正找到的解决方案。 这个讨论已经很长并且涉及很多人,我认为添加更多不会有帮助。

@Ismael-VC 很好,请随时进行投票。 事实上,我过去曾就问题进行过几次涂鸦民意调查(例如 http://doodle.com/poll/s8734pcue8yxv6t4)。 根据我的经验,在投票中投票的人数与在问题线程中讨论的人数相同或更少。 这对于非常具体的、通常是肤浅的/语法问题是有意义的。 但是,当您只能从现有选项中进行选择时,民意调查将如何产生新的想法?

当然,这个问题的真正目标是消除隐式矢量化函数。 理论上, map的语法就足够了,因为所有这些函数在每种情况下都只是在执行map

我试图为此寻找现有的数学符号,但您往往会看到这样的评论,即操作太不重要而没有符号! 在数学上下文中的任意函数的情况下,我几乎可以相信这一点。 然而,最接近的似乎是 Hadamard 产品符号,它有一些概括: https ://en.wikipedia.org/wiki/Hadamard_product_ (matrices)#Analogous_Operations

正如@rfourquet 所建议的那样,这给我们留下了sin∘x 。 似乎不太有帮助,因为它需要 unicode 并且无论如何都不广为人知。

@nalimilan ,我认为您只需执行sin(0.5 * abs2(a-b)) ~ (a,b)即可转换为map((a,b)->sin(0.5 * abs2(a-b)), (a,b)) 。 不确定这是否完全正确,但我认为它会起作用。

我也担心过多地研究让我给你一个巨大的复杂表达和你完美的自动矢量化它为我的问题。 我认为这方面的最终解决方案是构建一个完整的表达式/任务 + 查询计划等 DAG。但我认为这比仅仅为map提供方便的语法要大得多。

@quinnj是的,这基本上是上面提出的over语法,除了中缀运算符。

严肃的评论:我认为如果你追求这个想法足够远,你可能会重新发明 SQL,因为 SQL 本质上是一种用于组合许多变量的元素函数的语言,这些变量随后通过逐行“向量化”应用。

@johnmyleswhite同意,开始看起来像 DSL aka Linq

在主题发布的主题上,您可以专门化 |> 'pipe' 运算符并获得地图样式功能。 您可以将其读取为将函数通过管道传输到数据。 作为额外的奖励,您可以使用它来执行功能组合。

julia> (|>)(x::Function, y...) = map(x, y... )
|> (generic function with 8 methods)

julia> (|>)(x::Function, y::Function) = (z...)->x(y(z...))
|> (generic function with 8 methods)

julia> sin |> cos |> [ 1,2,3 ]
3-element Array{Float64,1}:
  0.514395
 -0.404239
 -0.836022

julia> x,y = rand(3), rand(3)
([0.8883630054185454,0.32542923024720194,0.6022157767415313],    [0.35274912207468145,0.2331784754319688,0.9262490059844113])

julia> sin |> ( 0.5 *( abs( x - y ) ) )
3-element Array{Float64,1}:
 0.264617
 0.046109
 0.161309

@johnmyleswhite是的,但也有一些非常适中的中间目标。 在我的分支上,多操作向量化表达式的map版本已经比我们现在拥有的要快。 因此,弄清楚如何顺利过渡到它有些紧迫。

@johnmyleswhite不确定。 很多 SQL 都是关于选择、排序和合并行的。 在这里,我们只讨论逐元素应用函数。 此外,SQL 没有提供任何语法来区分归约(例如SUM )和元素操作(例如>LN )。 后者就像目前在 Julia 中一样简单地自动矢量化。

@JeffBezanson使用\circ的美妙之处在于,如果您将索引族解释为索引集中的函数(这是标准的数学“实现”),那么映射 _is_ 只是组合。 所以(sin ∘ x)(i)=sin(x(i)) ,或者,更确切地说是sin(x[i])

正如@mdcfrancis 所提到的,管道的使用本质上只是“图表顺序”组合,这通常是用数学中的(可能是胖的)分号完成的(或者特别是范畴论的 CS 应用)——但我们已经有了管道运营商,当然。

如果这些组合运算符中的任何一个都不好,那么可以使用其他一些。 例如,至少有一些作者使用低级的\cdot来进行抽象箭头/态射组合,因为它本质上是箭头的群(或多或少)的“乘法”。

如果有人想要一个 ASCII 模拟:还有一些作者实际上使用句点来表示乘法。 (我可能也见过一些人用它来作曲;不记得了。)

所以一个人可能有sin . x ......但我想这会令人困惑:-}

仍然……最后一个类比可能是一个真正早期提议的论据,即sin.(x) 。 (或者这可能是牵强的。)

我们换个角度试试,别拍我。

如果我们用collect(..(A,B)) == ((a[1],..., a[n]), (b[1], ...,b[n])) == zip(A,B)定义.. $ ,那么正式使用T[x,y,z] = [T(x), T(y), T(z)]它就拥有

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

这激发了至少一种不干扰数组构造语法的 map 语法。 使用::table扩展f[::(A,B)] = [f(a[i], b[j]) for i in 1:n, j in 1:n]至少会导致第二个有趣的用例。

仔细考虑你想问的问题。

@toivoh谢谢,我会的。 我目前正在评估几个民意调查/调查软件。 此外,我只会对首选语法进行调查,那些想要阅读整个线程的人只会这样做,我们不要假设没有其他人会对这样做感兴趣。

找到一个没有人真正找到的解决方案

@nalimilan我们中间没有人,就是这样。 :微笑:

当您所能做的就是从现有选项中挑选时,民意调查如何产生新的想法?
在投票中投票的人数与在问题线程中讨论的人数相同或更少。

@JeffBezanson我很高兴听到您已经完成了民意调查,继续加油!

  • 你是如何宣传你的民意调查的?
  • 从到目前为止我评估过的民意调查/调查软件中,kwiksurveys.com 允许用户添加他们自己的意见,而不是_没有一个选项适合我_选项。

sin∘x似乎没有太大帮助,因为它需要 Unicode 并且无论如何都不广为人知。

我们有这么多 Unicode,让我们使用它,我们甚至有很好的方法来使用它们来完成制表符,那么使用它们有什么问题呢?,如果不知道,让我们记录和教育,如果它不存在,有什么问题发明它? 我们真的需要等待别人发明并使用它,以便我们可以将其作为先例,然后再考虑它吗?

有先例,所以问题是它是Unicode? 为什么? 那么我们什么时候开始使用其他不广为人知的Unicode呢? 绝不?

按照这种逻辑,无论如何,朱莉娅并不广为人知,但是那些想学习的人会。 以我非常谦虚的观点,这对我来说没有意义。

很公平,我并不完全反对 。 要求 unicode 使用非常基本的功能只是反对它的一个标志。 不一定足以完全沉没。

使用/重载*作为 ASCII 替代品会完全疯狂吗? 我会说它在数学上是有道理的,但我想有时可能很难辨别它的含义……(再说一次,如果它仅限于map功能,那么map已经是 ASCII 替代品了,不是吗?)

使用\circ的美妙之处在于,如果您将索引族解释为索引集中的函数(这是标准的数学“实现”),那么映射_is_ 只是组合。

我不确定我买这个。

@hayd它的哪一部分? 一个索引族(例如,一个序列)可以被看作是索引集中的一个函数,或者它的映射变成了组合? 或者在这种情况下这是一个有用的观点?

我认为前两个(数学)点是没有争议的。 但是,是的,我不会强烈主张在这里使用它——它主要是一种“啊,有点合适!” 反应。

@mlhetland |> 非常接近 -> 并且在今天有效 - 它还具有正确关联的“优势”。

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

@mdcfrancis当然。 但这改变了我概述的构图解释。 也就是说, sin∘x将等同于x |> sin ,不是吗?

PS:也许它在“代数”中丢失了,但只允许类型化数组构造中的函数T[x,y,z]使得f[x,y,z][f(x),f(y),f(z)]直接给出

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

这是非常可读的,可以被视为语法..

这很聪明。 但我怀疑,如果我们能让它工作, sin[x...]真的会在冗长上输给sin(x)sin~x等。

语法[sin xs]怎么样?

这在语法上类似于数组推导[sin(x) for x in xs]

@mlhetland罪 |> x === 地图(罪,x)

那将是与当前函数链接含义相反的顺序。 并不是说我不介意为该运算符找到更好的用途,但需要一个过渡期。

@mdcfrancis是的,我知道这就是您的目标。 这逆转了事情(如@tkelman重申)wrt。 我概述的作文解释。

我认为集成矢量化和链接会非常酷。 我想知道单词是否是最清晰的运算符。
就像是:

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

连续的几个地图可以自动组合成一个地图以提高性能。 另请注意,我假设地图将具有某种自动广播功能。 在开始时将 [1, 2] 替换为 _ 可以改为构建匿名函数。 注意我正在使用 R 的 magrittr 规则进行链接(请参阅我在链接线程中的帖子)。

也许这开始看起来更像是 DSL。

我已经关注这个问题很长时间了,直到现在才发表评论,但是恕我直言,这开始失控了。

我强烈支持使用干净的 map 语法的想法。 我最喜欢@tkelman~的建议,因为它保留在 ASCII 中以实现这种基本功能,我非常喜欢sin~x 。 如上所述,这将允许非常复杂的单线样式映射。 使用sin∘x也可以。 对于更复杂的事情,我倾向于认为适当的循环更清晰(通常是最好的性能)。 我真的不喜欢太多“神奇”的广播,它使代码更难遵循。 显式循环通常更清晰。

这并不是说不应该添加这样的功能,而是让我们首先有一个简洁的map语法,特别是因为它即将变得超级快(来自我对jb/functions分支的测试) .

请注意,jb/functions 的效果之一是broadcast(op, x, y)具有与自定义版本x .op y一样好的性能,后者在op上手动专门化广播。

对于更复杂的事情,我倾向于认为适当的循环更清晰(通常是最好的性能)。 我真的不喜欢太多“神奇”的广播,它使代码更难遵循。 显式循环通常更清晰。

我不同意。 exp(2 * x.^2)完全可读,并且比[exp(2 * v^2) for v in x]更简洁。 恕我直言,这里的挑战是通过让人们使用前者(它分配副本并且不融合操作)来避免陷入困境:为此,我们需要找到一种足够短的语法,以便可以弃用慢速形式。

更多的想法。 调用函数时,您可能需要做几件事:

循环无参数(链)
仅循环通过链接的参数(映射)
循环遍历所有参数(mapall)

以上每一项都可以通过以下方式进行修改:
标记要循环的项目 (~)
标记一个不被循环的项目(额外的一组 [ ] )

可统一的项目应该自动处理,而不考虑语法。
如果至少有两个参数正在循环,则应该自动扩展单例维度

只有当存在维度时,广播才会产生影响
否则不匹配。 所以当你说不广播时,你的意思是给一个
如果参数的大小不匹配,则会出现错误?

sin[x...] 在冗长方面确实输给了 sin(x) 或 sin~x 等。

此外,继续思考,地图sin[x...][f(x...)]上的一个不那么急切的版本。
语法

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

或类似[exp(2 * (x..)^2)]的东西将可用并且自我解释是否引入了真正的默认函数链接。

@nalimilan是的,但这符合我的“单线”类别,我说没有循环就可以了。

虽然我们列出了我们所有的愿望:对我来说更重要的是map的结果无需分配或复制即可分配。 这是我仍然更喜欢性能关键代码的循环的另一个原因,但如果可以减轻这种情况(#249 目前看起来不希望 ATM),那么这一切都会变得更具吸引力。

map 的结果无需分配或复制即可分配

你能扩展一下吗? 您当然可以改变map的结果。

我认为他的意思是将map的输出存储到预先分配的数组中。

对,就是这样。 道歉,如果这已经是可能的。

啊,当然。 我们有map! ,但正如您所观察到的,#249 正在寻求一些更好的方法来做到这一点。

@jtravs我在上面用LazyArray (https://github.com/JuliaLang/julia/issues/8450#issuecomment-65106563)提出了一个解决方案,但到目前为止性能并不理想。

@toivoh我在发布该帖子后对其进行了多次编辑。 我担心的问题是如何确定循环哪些参数以及不循环哪些参数(因此 mapall 可能比广播更清晰)。 我认为,如果您要遍历多个参数,则应始终在必要时扩展单例维度以生成可比较的数组,我认为。

是的map!是完全正确的。 如果这里制定的任何好的语法糖也涵盖了这种情况,那就太好了。 难道我们不能让x := ...将 RHS 隐式映射到x上。

我提出了一个名为 ChainMap 的包,它集成了映射和链接。

这是一个简短的示例:

<strong i="7">@chain</strong> begin
  [1, 2]
  -(1)
  (_, _)
  map_all(+)
  <strong i="8">@chain_map</strong> begin
    -(1)
    ^(2. , _)
  end
  begin
    a = _ - 1
    b = _ + 1
    [a, b]
  end
  sum
end

我一直在思考它,我想我终于找到了一种一致的儒略语法来映射从数组理解派生的数组。 下面的提议是 julian 的,因为它建立在已经建立的数组理解语言之上。

  1. @Jutho实际提出的f[a...]开始,约定
    对于 a 向量 a, b
f[a...] == map(f, a[:])
f[a..., b...] == map(f, a[:], b[:])
etc

它没有引入新的符号。

2.)除此之外,我建议引入一个额外的运算符:一个 _shape preserve_ splatting 运算符.. (比如说)。 这是因为...是一个 _flat_ 喷溅运算符,因此即使an维, f[a...]也应该返回一个向量而不是数组。 如果选择.. ,那么在这种情况下,

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

并且结果继承了参数的形状。 允许广播

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

会让写作的想法像

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

赫里卡?

这对映射表达式没有帮助,不是吗? over语法的优点之一是它如何与表达式一起工作:

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

好吧,如果你想允许的话,可能通过上面的[sin(x.. * y..)]sin[x.. * y..] 。 比起 over 语法,我更喜欢它,因为它提供了一个视觉提示,即函数运算符是在元素上而不是在容器上。

但是你能不能简化一下,让x..简单地映射到x上? 所以@johansigfrids 的例子是:

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

@jtravs因为范围界定( [println(g(x..))]println([g(x..)]) )和一致性[x..] = x

另一种可能性是将x.. = x[:, 1], x[:, 2], etc.作为前导子数组(列)的部分碎片,将..y作为尾随子阵列..y = y[1,:], y[2,:]的部分碎片。 如果两者都运行在不同的索引上,这涵盖了许多有趣的情况

[f(v..)] == [f(v[i]) for i in 1:m ]
[v.. * v..] == [v[i] * v[i] for 1:m]
[v.. * ..v] == [v[i] * v[j] for i in 1:m, j in 1:n]
[f(..A)] == [f(A[:, j]) for j in 1:n]
[f(A..)] == [f(A[i, :]) for i in 1:m]
[dot(A.., ..A)] == [dot(A[:,i], A[j,:]) for i in 1:m, j in 1:n] == A*A
[f(..A..)] == [f(A[i,j]) for i in 1:m, j in 1:n]
[v..] == [..v] = v
[..A..] == A

v一个向量, A一个矩阵)

我更喜欢over ,因为它允许您以正常语法编写表达式,而不是引入大量方括号和点。

我认为你对混乱的看法是对的,我试图调整和系统化我的建议。 为了不让每个人的耐心进一步过度,我在要点https://gist.github.com/mschauer/b04e000e9d0963e40058中写下了我对地图和索引等的想法。

在阅读完这个帖子之后,到目前为止,我的偏好是将 _both_ f.(x)用于简单的事情并且人们习惯于矢量化函数(“ . = vectorized”习语很常见),并且f(x^2)-x over x用于更复杂的表达式。

来自 Matlab、Numpy 等的人太多了,以至于完全放弃了向量化函数语法; 告诉他们添加点很容易记住。 用于将复杂表达式向量化为单个循环的良好的类似over的语法也非常有用。

over语法真的让我很反感。 我突然想到为什么:它假定表达式中每个变量的所有用法都是矢量化或非矢量化的,但情况可能并非如此。 例如, log(A) .- sum(A,1) – 假设我们删除了log的向量化。 您也不能通过表达式对函数进行向量化,这似乎是一个相当大的缺点,如果我想编写exp(log(A) .- sum(A,1))并且将explog向量化并且sum不是吗?

@StefanKarpinski ,那么您应该执行exp.(log.(A) .- sum(A,1))并接受额外的临时变量(例如,在性能不重要的交互使用中)或s = sum(A, 1); exp(log(A) - s) over A (尽管如果sum(A,1)是一个向量,你想要广播); 你可能只需要使用理解。 无论我们提出什么语法,我们都不会涵盖所有可能的情况,并且您的示例特别有问题,因为任何“自动”语法都必须知道sum是纯的并且它可以是吊出循环/地图。

对我来说,第一要务是broadcast(f, x...)map(f, x...)f.(x...)语法,这样我们就可以摆脱@vectorize 。 之后,我们可以继续研究像over (或其他)这样的语法来缩写map和推导式的更一般用途。

@stevengj我不认为那里的第二个例子有效,因为-不会广播。 假设A是一个矩阵,则输出将是一个单行矩阵的矩阵,每个矩阵都是A元素的对数减去沿第一维的和向量。 你需要broadcast((x, y)->exp(log(x)-y), A, sum(A, 1)) 。 但我认为map的简洁语法很有用,而且broadcast也不一定需要简洁的语法。

历史上像sin这样的自动向量化函数会在新语法中继续如此,还是会被弃用? 我担心即使是f.语法也会让大量没有概念优雅论点的科学程序员感到“陷阱”。

我的感觉是,像sin这样的历史矢量化函数应该被弃用,取而代之的是sin. ,但它们应该被准永久地弃用(而不是在后续版本中被完全删除)用户从其他科学语言中受益。

f.(args...)的一个小问题(?):尽管object.(field)语法在大多数情况下很少使用,并且可以用getfield(object, field)替换而不会有太多痛苦,但有一个_lot_ 形式的方法定义/引用Base.(:+)(....) = .... ,将这些更改为getfield会很痛苦。

一种解决方法是:

  • Base.(:+) $ 像所有其他f.(args...) $ 一样变成map(Base, :+) #$ ,但定义一个不推荐使用的方法map(m::Module, s::Symbol) = getfield(m, s)以实现向后兼容性
  • 支持语法Base.:+ (当前失败)并在Base.(:+)的弃用警告中推荐此语法

我想再问一次——这是否是我们在 0.5.0 中可以做的事情? 我认为这很重要,因为许多矢量化构造函数已被弃用。 我以为我会接受这个,但我确实发现map(Int32, a) ,而不是int32(a)有点乏味。

这基本上只是此时选择语法的问题吗?

这基本上只是此时选择语法的问题吗?

我认为@stevengj在他的 PR https://github.com/JuliaLang/julia/pull/15032中给出了支持编写sin.(x)而不是.sin(x)的好论据。 所以我会说这条路已经被清除了。

我对我们还没有一种解决方案来有效地将这种语法推广到复合表达式这一事实有所保留。 但我认为在这个阶段,我们最好合并这个涵盖大多数用例的特性,而不是让这个讨论无限期地悬而未决。

@JeffBezanson我正在将此里程碑恢复为 0.5.0,以便在分类讨论中提出它 - 主要是为了确保我不会忘记。

#15032 是否也适用于call - 例如Int32.(x)

@ViralBShah ,是的。 无论 f 的类型如何,任何f.(x...)都会在语法级别转换为map(f, broadcast, x...) f

这是.相对于f[x...]之类的主要优势,它在其他方面很有吸引力(并且不需要解析器更改)但仅适用于f::Functionf[x...]在概念上也与T[...]数组推导有些冲突。 虽然我认为@StefanKarpinski喜欢括号语法?

(再举一个例子,PyCall 中的o::PyObject对象是可调用的,调用 Python 对象o__call__方法,但相同的对象也可能支持o[...]索引。这会与f[x...]广播发生冲突,但与o.(x...)广播会正常工作。)

call不再存在。

(我也喜欢@nalimilan的论点,即f.(x...)使.(类似于.+等)

是的,明智的类比也是我最喜欢的类比。 我们可以继续合并吗?

带有模块的 getfield 是否应该被实际弃用?

@tkelman ,而不是什么? 但是, Base.(:+) (即文字符号参数)的弃用警告应该建议Base.:+ ,而不是getfield 。 (_Update_:还需要语法弃用来处理方法定义。)

@ViralBShah ,在周四的分诊讨论中对此有何决定? 我认为#15032 的合并状态非常好。

我认为 Viral 错过了电话的那一部分。 我的印象是,很多人仍然对f.(x)的美学持保留态度,并且可能更喜欢两者之一

  1. 一个中缀运算符,它在概念上和实现上会更简单,但是从我所见,我们没有任何可用的 ascii 运算符。 我之前不赞成对~进行宏解析的想法需要在包中进行替换,并且在这个周期中尝试这样做可能为时已晚。
  2. 或一种替代的新语法,可以更容易地融合循环和消除临时性。 在接近 #15032 水平的任何地方都没有实施其他替代方案,所以看起来我们应该合并它并尝试一下,尽管仍有保留。

是的,我确实有一些保留意见,但我现在找不到比f.(x)更好的选择。 这似乎比选择像~这样的任意符号更好,而且我敢打赌,许多习惯于.* (等)的人甚至可以立即猜出它的含义。

我想更好地了解的一件事是人们是否可以使用.( _replacing_ 现有的矢量化定义。 如果人们不喜欢它来代替它,我会更加犹豫。

作为一个潜伏在这个讨论中的用户,我非常想用它来替换我现有的矢量化代码。

我主要在 Julia 中使用矢量化来提高可读性,因为循环速度很快。 所以我很喜欢将它用于 exp、sin 等,就像前面提到的那样。 因为我已经在这样的表达式中使用了 .^, .* ,所以在 sin 中添加了额外的点。 exp。 etc 对我来说真的很自然,甚至更明确……尤其是当我可以使用一般符号轻松折叠自己的函数而不是混合 sin(x) 和 map(f, x) 时。

总而言之,作为普通用户,我真的,真的希望这被合并!

我更喜欢建议的fun[vec]语法而不是fun.(vec)
你觉得[fun vec]怎么样? 这就像一个列表推导式,但带有一个隐式变量。 它可以允许做T[fun vec]

对于长度 > 1 的向量,该语法在 Julia 0.4 中是免费的:

julia> [sin rand(1)]
1x2 Array{Any,2}:
 sin  0.0976151

julia> [sin rand(10)]
ERROR: DimensionMismatch("mismatch in dimension 1 (expected 1 got 10)")
 in cat_t at abstractarray.jl:850
 in hcat at abstractarray.jl:875

[fun over vec]这样的东西可以在语法级别进行转换,也许值得简化[fun(x) for x in vec]但并不比map(fun,vec)简单。

类似于[fun vec]的语法:语法(fun vec)是免费的,而{fun vec}已被弃用。

julia> (fun vec)
ERROR: syntax: missing separator in tuple

julia> {fun vec}

WARNING: deprecated syntax "{a b ...}".
Use "Any[a b ...]" instead.
1x2 Array{Any,2}:
 fun  [0.3231600663395422,0.10208482721149204,0.7964663210635679,0.5064134055014935,0.7606900072242995,0.29583012284224064,0.5501131920491444,0.35466150455688483,0.6117729165962635,0.7138111929010424]

@diegozeafun[vec]被排除在外,因为它与T[vec]冲突。 (fun vec)基本上是 Scheme 语法,多参数情况大概是(fun vec1 vec2 ...) ... 这与任何其他 Julia 语法都非常不同。 还是您打算(fun vec1, vec2, ...)与元组语法冲突? 也不清楚与fun.(vecs...)相比有什么优势。

此外,请记住,一个主要目标是最终有一个语法来替换@vectorized函数(这样我们就没有“适用于向量”的函数的“幸运”子集),这意味着对于习惯于在 Matlab、Numpy 等中使用矢量化函数的人来说,语法需要是可口/直观/方便的。 对于像sin(A .+ cos(B[:,1]))这样的表达式,它还需要易于组合。 这些要求排除了许多更具“创造性”的提议。

毕竟, sin.(A .+ cos.(B[:,1]))看起来并没有那么糟糕。 这将需要一个好的文档。 f.(x)将被记录为.(类似于.+吗?
可以弃用.+以支持+.吗?

# Since 
sin.(A .+ cos.(B[:,1]))
# could be written as
sin.(.+(A, cos.(B[:,1])))
# +.
sin.(+.(A, cos.(B[:,1]))) #  will be more coherent.

@diegozea ,#15032 已经包含文档,但欢迎提供任何其他建议。

.+将继续拼写为.+ 。 首先,这个点的位置太根深蒂固了,在这里改变拼写并没有足够的收益。 其次,正如@nalimilan指出的那样,您可以将.(视为“矢量化函数调用运算符”,从这个角度来看,语法已经是一致的。

(一旦broadcast (#4883) 中类型计算的困难得到解决,我希望再做一次 PR,以便任何运算符 a .⧆ b糖对于broadcast(⧆, a, b)的调用。这样,我们将不再需要显式地实现.+等——您将通过定义+等自动获得广播运算符。我们将仍然能够通过为特定的运算符重载broadcast来实现专门的方法,例如对 BLAS 的调用。)

对于像sin(A .+ cos(B[:,1]))这样的表达式,它还需要易于组合。

是否可以将f1.(x, f2.(y .+ z))解析为broadcast((a, b, c)->(f1(a, f2(b + c))), x, y, z)

编辑:我看到上面已经提到了......在@github默认隐藏的评论中......

@yuyichao ,如果函数被标记为@pure (至少如果 eltypes 是不可变的),循环融合似乎应该是可能的,正如我在 #15032 中评论的那样,但这是编译器的任务,而不是解析器。 (但像这样的向量化语法更多是为了方便,而不是为了将最后一个循环挤出关键的内部循环。)

请记住,这里的关键目标是消除对@vectorized函数的需求; 这需要语法至少一样通用,几乎一样方便,至少一样快。 它不需要自动循环融合,尽管将用户的broadcast意图暴露给编译器以在将来的某个日期打开循环融合的可能性是很好的。

如果它也进行循环融合有什么缺点吗?

@yuyichao ,循环融合是一个更难的问题,即使抛开非纯函数也不总是可能的(例如,参见上面@StefanKarpinskiexp(log(A) .- sum(A,1))示例)。 在我看来,坚持实现它可能会导致它_永远_不会被实现——我们必须逐步地做到这一点。 首先暴露用户的意图。 如果以后能进一步优化,那就太好了。 如果没有,我们仍然可以对现在可用的少数“矢量化”函数进行通用替代。

另一个障碍是.+等当前没有作为broadcast操作暴露给解析器; .+只是另一个功能。 如上所述,我的计划是改变这一点(为broadcast(+, ...)制作.+糖)。 但同样,如果更改是增量的,那么取得进展会容易得多。

我的意思是通过证明这样做是有效的来进行循环融合是很困难的,所以我们可以让解析器将转换作为原理图的一部分。 在上面的例子中,它可以写成。 exp.(log.(A) .- sum(A,1))并被解析为broadcast((x, y)->exp(log(x) - y), A, sum(A, 1))

如果.+不属于同一类别也很好(就像任何非板载函数调用将被放入参数中一样),如果我们只在一个以后的版本。 我主要是问在解析器中是否有可能(即明确)有这样的示意图,以及通过这种方式允许编写矢量化和融合循环是否有任何缺点..

通过证明这样做是有效的来进行循环融合是困难的

我的意思是在编译器中这样做很难(也许并非不可能),特别是因为编译器需要查看broadcast的复杂实现,除非我们在编译器中特殊情况下broadcast ,即可能是个坏主意,如果可能,我们应该避免它......

可能是? 这是一个有趣的想法,并且以这种方式将.(语法定义为“融合”似乎并非不可能,并留给调用者不要将其用于不纯函数。 最好的办法是尝试一下,看看是否有任何困难的案例(我现在没有看到任何明显的问题),但我倾向于在“非融合”PR 之后这样做。

我倾向于在“非融合”公关之后这样做。

完全同意,特别是因为无论如何都没有处理.+

我不想破坏这一点,但@yuyichao的建议给了我一些想法。 这里的重点是对哪些函数进行了矢量化,但这对我来说似乎总是有点错位——真正的问题是要对哪些变量进行矢量化,这完全决定了结果的形状。 这就是为什么我倾向于为矢量化标记参数,而不是为矢量化标记函数。 标记参数还允许对一个参数进行矢量化而不是另一个参数的函数。 也就是说,我们可以两者兼得,而且这个 PR 的直接目的是替换内置的矢量化函数。

@StefanKarpinski ,当您调用f.(args...)broadcast(f, args...)时,它会将 _all_ 参数向量化。 (为此,请记住标量被视为 0 维数组。)在@yuyichaof.(args...) = _fused 广播语法_(我越来越喜欢它)的建议中,我认为融合会“在任何不是func.(args...)的表达式处停止”(将来包括.+等)。

因此,例如, sin.(x .+ cos.(x .^ sum(x.^2)))将(在julia-syntax.scm中)变成broadcast((x, _s_) -> sin(x + cos(x^_s_)), x, sum(broacast(^, x, 2))) 。 请注意, sum函数将是“融合边界”。 在融合会破坏副作用的情况下,调用者将负责不使用f.(args...)

你有一个例子,这还不够吗?

我越来越喜欢

我很高兴你喜欢它。 =)

只是另一个可能不属于同一轮的扩展,可以使用.=.*=或类似的方法来解决就地分配问题(通过使其与正常分配)

是的,其他操作缺乏融合是我在 #7052 中对.+=等的主要反对意见,但我认为这可以通过将.=与其他func.(args...)调用融合来解决. 或者只是融合x[:] = ...

:thumbsup: 在这个讨论中有两个概念挤在一起,实际上是非常正交的:
matlab'y “融合广播操作”或x .* y .+ z和 apl'y “产品和拉链上的地图”,如f[product(I,J)...]f[zip(I,J)...] 。 过去的谈话可能也与此有关。

@mschauer ,如果IJ具有相同的形状,则f.(I, J)已经(在 #15032 中)等效于map(x -> f(x...), zip(I, J) 。 如果I是一个行向量并且J是一个列向量,反之亦然,那么broadcast确实映射了产品集(或者你可以做f.(I, J')如果它们都是一维数组)。 所以我不明白你为什么认为这些概念“非常正交”。

正交不是正确的词,它们只是不同足以共存。

不过,关键是我们不需要为这两种情况使用单独的语法。 func.(args...)可以同时支持两者。

一旦三巨头的成员(Stefan、Jeff、Viral)合并 #15032(我认为它已准备好合并),我将关闭它并提交路线图问题以概述剩余的提议更改:修复广播类型计算,弃用@vectorize ,将.op变成广播糖,添加语法级别的“广播融合”,最后与就地分配融合。 最后两个可能不会进入0.5。

嘿,我对 15032 感到非常高兴和感激。不过,我不会对讨论不屑一顾。 例如,向量和类似对象的向量在 julia 中使用仍然非常尴尬,但作为理解的结果,可以像杂草一样发芽。 不基于将迭代编码为单一维度的良好隐式表示法有可能大大简化这一点,例如使用灵活的迭代器和新的生成器表达式。

我认为现在可以关闭这有利于#16285。

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