【问题标题】:Julia: How to generate new immutable struct by modifying original immutable struct at user-supplied field?Julia:如何通过在用户提供的字段中修改原始不可变结构来生成新的不可变结构?
【发布时间】:2020-07-31 19:43:29
【问题描述】:

假设我有一些不可变的结构,例如

struct Person
           name::Symbol
           age::Int
       end;

我想写一个函数

function copyWithModification(original_person::Person, fieldToChange::String, valueForNewField)::Person

它返回一个新的 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> 

我应该注意我不关心这段代码的性能;它只会被调用一次或两次。关键是要节省代码复制和人为错误,因为我实际要使用此代码的结构要大得多。

【问题讨论】:

    标签: struct julia metaprogramming


    【解决方案1】:

    如果您不关心性能,那么在您的情况下使用简单的内省很好并且非常简单:

    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)
    

    【讨论】:

      【解决方案2】:

      您不需要为此使用元编程。我认为这个“正常”功能可以满足您的需求。

      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”以使其成为另一个构造函数,但您可以将名称更改为您想要的名称。

      1https://docs.julialang.org/en/v1/base/base/#Base.fieldnames

      [2]https://docs.julialang.org/en/v1/base/base/#Base.getproperty

      [3]https://docs.julialang.org/en/v1/base/base/#...

      【讨论】:

        【解决方案3】:

        Setfield中已经定义了这样的函数,无需重新发明轮子!

        julia> using Setfield
        
        julia> p = Person(:Smith, 10)
        Person(:Smith, 10)
        
        julia> setproperties(p, age=20)
        Person(:Smith, 20)
        

        一次可以设置多个字段,有关详细信息,请参阅?setproperties。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多