Julia:如何通过修改用户提供的字段中的原始不可变结构来生成新的不可变结构?
Julia: How to generate new immutable struct by modifying original immutable struct at user-supplied field?
假设我有一些不可变的结构,例如
struct Person
name::Symbol
age::Int
end;
我想写一个函数
function copyWithModification(original_person::Person, fieldToChange::String, valueForNewField)::Person
那 returns 一个新的 Person 结构,就像旧的结构一样,只是 fieldToChange 中指定的字段的值已设置为 valueForNewField。我该怎么做?
我目前的尝试使用 Setfield 和元编程
using Setfield
function copyWithModification(original_person::Person, fieldToChange::String, valueForNewField)::Person
return eval(Meta.parse("@set original_person." * fieldToChange * " = " * string(valueForNewField)))
end
这不起作用,因为 eval 是在全局范围内执行的,因此无法访问 original_person 对象:
julia> struct Person
name::Symbol
age::Int
end;
julia> using Setfield
julia> function copyWithModification(original_person::Person, fieldToChange::String, valueForNewField)::Person
return eval(Meta.parse("@set original_person." * fieldToChange * " = " * string(valueForNewField)))
end
copyWithModification (generic function with 1 method)
julia> person_local_scope = Person(:test, 10)
Person(:test, 10)
julia> copyWithModification(person_local_scope, "age", 20)
ERROR: UndefVarError: original_person not defined
Stacktrace:
[1] top-level scope at /Users/lionstarr/.julia/packages/Setfield/XM37G/src/sugar.jl:182
[2] eval at ./boot.jl:330 [inlined]
[3] eval(::Expr) at ./client.jl:425
[4] copyWithModification(::Person, ::String, ::Int64) at ./REPL[3]:2
[5] top-level scope at REPL[5]:1
julia>
请注意,我不关心这段代码的性能;它只会被调用一次或两次。重点是保存代码复制和人为错误,因为我实际想要使用此代码的结构要大得多。
您不需要为此使用元编程。我认为这个“正常”功能可以满足您的需求。
function Person(p :: Person,fieldtochange,newvalue)
newparams = [] # This array will store a new list of parameters
# This loop will iterate in all the fields (obtained via [fieldnames][1])
# of the struct Person and compare with the given field,
# if it coincides, adds the new value to the newparams array,
# if not, get the values of the original person using
# getproperty and add them to the array.
for currentfield in fieldnames(Person)
if currentfield == fieldtochange
push!(newparams,newvalue)
else
push!(newparams,getproperty(p,currentfield)) #[2]
end
end
return Person(newparams...) #Construct a new person with the new parameters
# using '...' for [splatting][3].
end
在本例中,我将函数命名为“Person”,使其成为另一个构造函数,但您可以将名称更改为您想要的名称。
1 https://docs.julialang.org/en/v1/base/base/#Base.fieldnames
[2] https://docs.julialang.org/en/v1/base/base/#Base.getproperty
如果您不关心性能,在您的情况下使用简单的内省就可以而且非常简单:
function copy_with_modification1(original::T, field_to_change, new_value) where {T}
val(field) = field==field_to_change ? new_value : getfield(original, field)
T(val.(fieldnames(T))...)
end
例如,它会产生以下结果:
julia> struct Person
name::Symbol
age::Int
end
julia> p = Person(:Joe, 42)
Person(:Joe, 42)
julia> using BenchmarkTools
julia> @btime copy_with_modification1($p, :age, 43)
666.924 ns (7 allocations: 272 bytes)
Person(:Joe, 43)
为了重新获得效率,可以通过在编译时列出字段的方式实现同类技术。这是一个使用 generated function:
的例子
# Can't have closures inside generated functions, so the helper function
# is declared outside
function val_(original, field, field_to_change, new_value)
field == field_to_change ? new_value : getfield(original, field)
end
@generated function copy_with_modification2(original, field_to_change, new_value)
# This is the "compile-time" part
T = original # here `original` refers to the type of the argument
fields = fieldnames(T) # fieldnames is called compile-time
# This is the "run-time" part
quote
# We broadcast only over `fields`, other arguments are treated as scalars
$T(val_.(Ref(original), $fields, Ref(field_to_change), Ref(new_value))...)
end
end
现在性能好多了:
julia> @btime copy_with_modification2($p, :age, 43)
2.533 ns (0 allocations: 0 bytes)
Person(:Joe, 43)
这样的功能在Setfield
中已经定义好了,不用再重复造轮子了!
julia> using Setfield
julia> p = Person(:Smith, 10)
Person(:Smith, 10)
julia> setproperties(p, age=20)
Person(:Smith, 20)
一次可以设置多个字段,详情见?setproperties
。
假设我有一些不可变的结构,例如
struct Person
name::Symbol
age::Int
end;
我想写一个函数
function copyWithModification(original_person::Person, fieldToChange::String, valueForNewField)::Person
那 returns 一个新的 Person 结构,就像旧的结构一样,只是 fieldToChange 中指定的字段的值已设置为 valueForNewField。我该怎么做?
我目前的尝试使用 Setfield 和元编程
using Setfield
function copyWithModification(original_person::Person, fieldToChange::String, valueForNewField)::Person
return eval(Meta.parse("@set original_person." * fieldToChange * " = " * string(valueForNewField)))
end
这不起作用,因为 eval 是在全局范围内执行的,因此无法访问 original_person 对象:
julia> struct Person
name::Symbol
age::Int
end;
julia> using Setfield
julia> function copyWithModification(original_person::Person, fieldToChange::String, valueForNewField)::Person
return eval(Meta.parse("@set original_person." * fieldToChange * " = " * string(valueForNewField)))
end
copyWithModification (generic function with 1 method)
julia> person_local_scope = Person(:test, 10)
Person(:test, 10)
julia> copyWithModification(person_local_scope, "age", 20)
ERROR: UndefVarError: original_person not defined
Stacktrace:
[1] top-level scope at /Users/lionstarr/.julia/packages/Setfield/XM37G/src/sugar.jl:182
[2] eval at ./boot.jl:330 [inlined]
[3] eval(::Expr) at ./client.jl:425
[4] copyWithModification(::Person, ::String, ::Int64) at ./REPL[3]:2
[5] top-level scope at REPL[5]:1
julia>
请注意,我不关心这段代码的性能;它只会被调用一次或两次。重点是保存代码复制和人为错误,因为我实际想要使用此代码的结构要大得多。
您不需要为此使用元编程。我认为这个“正常”功能可以满足您的需求。
function Person(p :: Person,fieldtochange,newvalue)
newparams = [] # This array will store a new list of parameters
# This loop will iterate in all the fields (obtained via [fieldnames][1])
# of the struct Person and compare with the given field,
# if it coincides, adds the new value to the newparams array,
# if not, get the values of the original person using
# getproperty and add them to the array.
for currentfield in fieldnames(Person)
if currentfield == fieldtochange
push!(newparams,newvalue)
else
push!(newparams,getproperty(p,currentfield)) #[2]
end
end
return Person(newparams...) #Construct a new person with the new parameters
# using '...' for [splatting][3].
end
在本例中,我将函数命名为“Person”,使其成为另一个构造函数,但您可以将名称更改为您想要的名称。
1 https://docs.julialang.org/en/v1/base/base/#Base.fieldnames
[2] https://docs.julialang.org/en/v1/base/base/#Base.getproperty
如果您不关心性能,在您的情况下使用简单的内省就可以而且非常简单:
function copy_with_modification1(original::T, field_to_change, new_value) where {T}
val(field) = field==field_to_change ? new_value : getfield(original, field)
T(val.(fieldnames(T))...)
end
例如,它会产生以下结果:
julia> struct Person
name::Symbol
age::Int
end
julia> p = Person(:Joe, 42)
Person(:Joe, 42)
julia> using BenchmarkTools
julia> @btime copy_with_modification1($p, :age, 43)
666.924 ns (7 allocations: 272 bytes)
Person(:Joe, 43)
为了重新获得效率,可以通过在编译时列出字段的方式实现同类技术。这是一个使用 generated function:
的例子# Can't have closures inside generated functions, so the helper function
# is declared outside
function val_(original, field, field_to_change, new_value)
field == field_to_change ? new_value : getfield(original, field)
end
@generated function copy_with_modification2(original, field_to_change, new_value)
# This is the "compile-time" part
T = original # here `original` refers to the type of the argument
fields = fieldnames(T) # fieldnames is called compile-time
# This is the "run-time" part
quote
# We broadcast only over `fields`, other arguments are treated as scalars
$T(val_.(Ref(original), $fields, Ref(field_to_change), Ref(new_value))...)
end
end
现在性能好多了:
julia> @btime copy_with_modification2($p, :age, 43)
2.533 ns (0 allocations: 0 bytes)
Person(:Joe, 43)
这样的功能在Setfield
中已经定义好了,不用再重复造轮子了!
julia> using Setfield
julia> p = Person(:Smith, 10)
Person(:Smith, 10)
julia> setproperties(p, age=20)
Person(:Smith, 20)
一次可以设置多个字段,详情见?setproperties
。