Julia:非破坏性地更新不可变类型变量
Julia: non-destructively update immutable type variable
假设有一个类型
immutable Foo
x :: Int64
y :: Float64
end
还有一个变量foo = Foo(1,2.0)
。我想使用 foo
构造一个新变量 bar
作为原型,字段 y = 3.0
(或者,非破坏性地更新 foo
产生一个新的 Foo
对象).在 ML 语言(Haskell、OCaml、F#)和其他一些语言(例如 Clojure)中,有一个惯用语在伪代码中看起来像
bar = {foo with y = 3.0}
Julia 有这样的东西吗?
这很棘手。在 Clojure 中,这将与数据结构一起工作,一个动态类型的不可变映射,因此我们只需调用适当的方法来 add/change 一个键。但是在使用 types 时,我们必须进行一些反射,以便为该类型生成一个合适的新构造函数。此外,与 Haskell 或各种 ML 不同,Julia 不是静态类型的,因此人们不能简单地查看像 {foo with y = 1}
这样的表达式并计算出应该生成什么代码来实现它。
实际上,我们可以为此构建一个类似 Clojure 的解决方案;由于 Julia 提供了足够的反射和活力,我们可以将类型视为一种不可变的映射。我们可以使用 fieldnames
按顺序获取 "keys" 的列表(如 [:x, :y]
),然后我们可以使用 getfield(foo, :x)
动态获取字段值:
immutable Foo
x
y
z
end
x = Foo(1,2,3)
with_slow(x, p) =
typeof(x)(((f == p.first ? p.second : getfield(x, f)) for f in fieldnames(x))...)
with_slow(x, ps...) = reduce(with_slow, x, ps)
with_slow(x, :y => 4, :z => 6) == Foo(1,4,6)
然而,这被称为 with_slow
是有原因的。由于反射,它 远不及 像 withy(foo::Foo, y) = Foo(foo.x, y, foo.z)
这样的手写函数。如果 Foo
被参数化(例如 Foo{T}
和 y::T
),那么 Julia 将能够推断出 withy(foo, 1.)
returns a Foo{Float64}
,但不会根本无法推断出 with_slow
。正如我们所知,这会破坏 crab 性能。
使它与 ML 和 co 一样快的唯一方法是生成与手写版本有效等效的代码。碰巧,我们也可以推出那个版本!
# Fields
type Field{K} end
Base.convert{K}(::Type{Symbol}, ::Field{K}) = K
Base.convert(::Type{Field}, s::Symbol) = Field{s}()
macro f_str(s)
:(Field{$(Expr(:quote, symbol(s)))}())
end
typealias FieldPair{F<:Field, T} Pair{F, T}
# Immutable `with`
for nargs = 1:5
args = [symbol("p$i") for i = 1:nargs]
@eval with(x, $([:($p::FieldPair) for p = args]...), p::FieldPair) =
with(with(x, $(args...)), p)
end
@generated function with{F, T}(x, p::Pair{Field{F}, T})
:($(x.name.primary)($([name == F ? :(p.second) : :(x.$name)
for name in fieldnames(x)]...)))
end
第一部分是生成类符号对象的技巧,f"foo"
,其值在类型系统中是已知的。生成的函数就像一个宏,它接受类型而不是表达式;因为它可以访问 Foo
和字段名称,所以它基本上可以生成此代码的手动优化版本。您还可以检查 Julia 是否能够正确推断输出类型,如果您设置参数 Foo
:
@code_typed with(x, f"y" => 4., f"z" => "hello") # => ...::Foo{Int,Float64,String}
(for nargs
行本质上是一个手动展开的 reduce,可以实现这一点。)
最后,为了避免有人指责我给出了有点疯狂的建议,我想警告说这在 Julia 中并不是那么惯用。虽然我不能在不了解您的用例的情况下给出非常具体的建议,但通常最好是拥有一组可管理的(小的)字段和一组对这些字段进行基本操作的函数;您可以在这些函数的基础上创建最终的 public API。如果你真正想要的是一个不可变的字典,你最好只使用专门的数据结构。
FixedSizeArrays.jl
包中还实现了 setindex
(末尾没有 !
),它以一种有效的方式实现了这一点。
假设有一个类型
immutable Foo
x :: Int64
y :: Float64
end
还有一个变量foo = Foo(1,2.0)
。我想使用 foo
构造一个新变量 bar
作为原型,字段 y = 3.0
(或者,非破坏性地更新 foo
产生一个新的 Foo
对象).在 ML 语言(Haskell、OCaml、F#)和其他一些语言(例如 Clojure)中,有一个惯用语在伪代码中看起来像
bar = {foo with y = 3.0}
Julia 有这样的东西吗?
这很棘手。在 Clojure 中,这将与数据结构一起工作,一个动态类型的不可变映射,因此我们只需调用适当的方法来 add/change 一个键。但是在使用 types 时,我们必须进行一些反射,以便为该类型生成一个合适的新构造函数。此外,与 Haskell 或各种 ML 不同,Julia 不是静态类型的,因此人们不能简单地查看像 {foo with y = 1}
这样的表达式并计算出应该生成什么代码来实现它。
实际上,我们可以为此构建一个类似 Clojure 的解决方案;由于 Julia 提供了足够的反射和活力,我们可以将类型视为一种不可变的映射。我们可以使用 fieldnames
按顺序获取 "keys" 的列表(如 [:x, :y]
),然后我们可以使用 getfield(foo, :x)
动态获取字段值:
immutable Foo
x
y
z
end
x = Foo(1,2,3)
with_slow(x, p) =
typeof(x)(((f == p.first ? p.second : getfield(x, f)) for f in fieldnames(x))...)
with_slow(x, ps...) = reduce(with_slow, x, ps)
with_slow(x, :y => 4, :z => 6) == Foo(1,4,6)
然而,这被称为 with_slow
是有原因的。由于反射,它 远不及 像 withy(foo::Foo, y) = Foo(foo.x, y, foo.z)
这样的手写函数。如果 Foo
被参数化(例如 Foo{T}
和 y::T
),那么 Julia 将能够推断出 withy(foo, 1.)
returns a Foo{Float64}
,但不会根本无法推断出 with_slow
。正如我们所知,这会破坏 crab 性能。
使它与 ML 和 co 一样快的唯一方法是生成与手写版本有效等效的代码。碰巧,我们也可以推出那个版本!
# Fields
type Field{K} end
Base.convert{K}(::Type{Symbol}, ::Field{K}) = K
Base.convert(::Type{Field}, s::Symbol) = Field{s}()
macro f_str(s)
:(Field{$(Expr(:quote, symbol(s)))}())
end
typealias FieldPair{F<:Field, T} Pair{F, T}
# Immutable `with`
for nargs = 1:5
args = [symbol("p$i") for i = 1:nargs]
@eval with(x, $([:($p::FieldPair) for p = args]...), p::FieldPair) =
with(with(x, $(args...)), p)
end
@generated function with{F, T}(x, p::Pair{Field{F}, T})
:($(x.name.primary)($([name == F ? :(p.second) : :(x.$name)
for name in fieldnames(x)]...)))
end
第一部分是生成类符号对象的技巧,f"foo"
,其值在类型系统中是已知的。生成的函数就像一个宏,它接受类型而不是表达式;因为它可以访问 Foo
和字段名称,所以它基本上可以生成此代码的手动优化版本。您还可以检查 Julia 是否能够正确推断输出类型,如果您设置参数 Foo
:
@code_typed with(x, f"y" => 4., f"z" => "hello") # => ...::Foo{Int,Float64,String}
(for nargs
行本质上是一个手动展开的 reduce,可以实现这一点。)
最后,为了避免有人指责我给出了有点疯狂的建议,我想警告说这在 Julia 中并不是那么惯用。虽然我不能在不了解您的用例的情况下给出非常具体的建议,但通常最好是拥有一组可管理的(小的)字段和一组对这些字段进行基本操作的函数;您可以在这些函数的基础上创建最终的 public API。如果你真正想要的是一个不可变的字典,你最好只使用专门的数据结构。
FixedSizeArrays.jl
包中还实现了 setindex
(末尾没有 !
),它以一种有效的方式实现了这一点。