关于结构体根据配置初始化的问题


#1

这个问题在上次北京的线下meetup上,跟 @thautwarm 有过简短的讨论, 不过没有深入探讨,这里整理出来,看看大家有没有什么建议。

问题描述

我在实现一些算法的时候,经常需要将多个子模块嵌套在一起,比如,我有以下几个算法:

struct A
  a1
  a2
end

A(;a1=1, a2=2) = A(a1, a2)

struct B
  a::A
  b
end

B(;a=A(), b=2) = B(a, b)

struct C
  a::A 
  b::B 
  c
end

C(;a=A(),b=B(),c=3) = C(a,b,c)

这里,C模块是主要跟用户打交道的,然后现在有这样的需求,我希望获取一个跟C很像的一个拷贝,但是需要对c.b.a.a1以及c.b.b赋予一个新的值。最直接的做法是,把这个过程交给用户去处理,然后,只扩展similar(或者copy

c = C()
c_like = similar(c)
c.b.a.a1 = 0
c.b.b = 0

我想达到的效果是,

c = C()
c_like = similar(c; b.a.a1=0, b.b=0)

不过,这里显然有问题,b.a.a1类似的并不能作为参数名。

Q1: 不知道这里有没有更好的做法?

类似的实现

更一般地,有时候希望直接通过配置文件就完成初始化。在Python中,有这么一个库来完成类似的功能:

通过配置文件,指定初始化时候的参数。

Q2: 在Julia中,有没有必要这样子做?
Q3: 如果确实有必要的话,怎么实现?


#2

如果结构是mutable的话也许可以用宏简单糊一下?

julia> macro apply(obj,blk)
           @assert blk.head==:block
           map(blk.args) do line
               if line isa Expr
                   if line.head==:(=)
                       par=line
                       cur=line.args[1]
                       while cur isa Expr&&cur.head==:.
                           par=cur
                           cur=cur.args[1]
                       end
                       par.args[1]=Expr(:.,esc(obj),QuoteNode(cur))
                   end
               end
           end
           blk
       end
@apply (macro with 1 method)

julia> c=C()
C(A(1, 2), B(A(1, 2), 2), 3)

julia> @apply c begin
           b.a.a1=0
           b.b=0
       end
0

julia> c
C(A(1, 2), B(A(0, 2), 0), 3)

如果限制immutable的话就有点麻烦了


#3

这也确实是个思路。

照这么做,对于immutable的struct,确实有点麻烦,需要根据参数的path,自动找到对应的结构体,生成构造函数。


#4

immutable的话因为必须一次性初始化完,所以只用宏应该是不行了,要用生成函数拿到类型,然后用反射拿到每个类型的所有字段,如有手工指定的部分就用指定值,否则从源对象取值


#5

这个东西叫lens…


#6

可以把它做成chaining的,专治immutable:


#7

:sob: :sob: :sob:

是的是的是的


#8

用 “julia lens” 做关键词搜了下,找到了这么个库:

以及这里的讨论:


#9

结论:

julia> @with C().a.a1 = 3
C(A(3, 2), B(A(1, 2), 2), 3)

julia> c = C()
C(A(1, 2), B(A(1, 2), 2), 3)

julia> c = @with c.a.a1 = 3
C(A(3, 2), B(A(1, 2), 2), 3)

julia> c = @with c.a.a2 = 5
C(A(3, 5), B(A(1, 2), 2), 3)

julia> c = @with c.b.a.a2 = 5
C(A(3, 5), B(A(1, 5), 2), 3)

怎么实现的? 思考例子

# a.b.c.d <- v
let cache = a
    # cache.c.d <- v
    value =
        let cache = cache.b,
            # cache.d <- v
            value =
                let cache = cache.c
                    value = v
                    value = update(cache, Val(d), v)
                end
            update(cache, Val(c), value)
        end
    update(cache, Val(b), value)
end

得到一个33行代码的实现:

using MLStyle
@generated function field_update(main :: T, field::Val{Field}, value) where {T, Field}
    fields = fieldnames(T)
    quote
        $T($([field !== Field ? :(main.$field) : :value for field in fields]...))
    end
end

function lens_compile(ex, cache, value)
    @when :($a.$(b::Symbol).$(c::Symbol) = $d) = ex begin
        updated =
            Expr(:let,
                Expr(:block, :($cache = $cache.$b), :($value = $d)),
                :($field_update($cache, $(Val(c)), $value)))
        lens_compile(:($a.$b = $updated), cache, value)
    @when :($a.$(b::Symbol) = $c) = ex
        Expr(:let,
            Expr(:block, :($cache = $a), :($value=$c)),
            :($field_update($cache, $(Val(b)), $value)))
    @otherwise
        error("Malformed update notation $ex, expect the form like 'a.b = c'.")
    end
end

function with(ex)
    cache = gensym("cache")
    value = gensym("value")
    lens_compile(ex, cache, value)
end

macro with(ex)
    with(ex) |> esc
end

function with(ex)
    cache = gensym("cache")
    value = gensym("value")
    lens_compile(ex, cache, value)
end

macro with(ex)
    with(ex) |> esc
end

#10

Thanks!!!

我应该可以在这个基础上,做一些个性化的定制了


再抛一个更challenge的问题 :blush:

假如现在我想一次性修改多个field,比如c = @with c.a.a1 = 3 c.b.a.a1 = 5 c.b.a.a2=8,简单的做法就是,直接递归套用多次@with,但是,显然, 后两个赋值语句可以放在一个构造函数里…


#11

是的, 先想好语法, 比如 @with c.a.{a2 = v1, o = v2}, 这个表示{}共用一个cache.

我试一下, 不会很麻烦的


#12

可能是我用得不对,不过有个例子不工作

julia> using DiffEqBase

julia> f = ODEFunction((du,u,p,t)->du.=u);

julia> @with f.jac = (J,u,p,t)->fill!(J, 0);
ERROR: MethodError: Cannot `convert` an object of type getfield(Main, Symbol("##61#62")) to an object of type Nothing
Closest candidates are:
  convert(::Type{Nothing}, ::Any) at some.jl:25
  convert(::Type{Union{Nothing, T}}, ::Any) where T at some.jl:23
  convert(::Type{Nothing}, ::Nothing) at some.jl:24
  ...
Stacktrace:
 [1] convert(::Type{Nothing}, ::Function) at ./some.jl:25
...

#13

MWE:

julia> struct Foo{T}
           x::T
       end

julia> x = Foo(1)
Foo{Int64}(1)

julia> @with x.x = 1
Foo{Int64}(1)

julia> @with x.x = 0.2
ERROR: InexactError: Int64(0.2)
Stacktrace:
 [1] Int64 at ./float.jl:709 [inlined]
 [2] convert at ./number.jl:7 [inlined]
 [3] Foo at ./REPL[44]:2 [inlined]
 [4] macro expansion at ./REPL[39]:5 [inlined]
 [5] field_update(::Foo{Int64}, ::Val{:x}, ::Float64) at ./REPL[39]:2
 [6] top-level scope at REPL[47]:1

#14

嗯,我也发现了,我上面贴的那个discourse的链接里有讨论这个问题

主要是因为 field_update里用了T()作为构造函数,而T对应原来的类型。


#15

我觉得就是缺一个interface,从type得到真正的constructor的函数。比如说

julia> struct Goo{A,T}
           x::T
       end

julia> construct_from_type(::Type{<:Goo}, x) = Goo{true, typeof(x)}(x)
construct_from_type (generic function with 1 method)

julia> @generated function field_update(main :: T, field::Val{Field}, value) where {T, Field}
           fields = fieldnames(T)
           quote
               construct_from_type($T, $([field !== Field ? :(main.$field) : :value for field in fields]...)...)
           end
       end
field_update (generic function with 1 method)

julia> x = Goo{true, Int}(1)
Goo{true,Int64}(1)

julia> @with x.x = 0.2
Goo{true,Float64}(0.2)

#16

a.{b=c} 有点难写… 先睡觉去了…


#17

@Scheme

还有一个办法:

function generalise end

@generated function field_update(main :: T, field::Val{Field}, value) where {T, Field}
    fields = fieldnames(T)
    quote
        $(generalise(T))($([field !== Field ? :(main.$field) : :value for field in fields]...))
    end
end

然后

struct Goo{T}
           x::T
end
generalise(::Type{<:Goo}) = Goo

这个只要是参数能推结构体类型就能用