mobiuseng
mobiuseng

Reputation: 2396

Julia: non-destructively update immutable type variable

Let's say there is a type

immutable Foo
    x :: Int64
    y :: Float64
end

and there is a variable foo = Foo(1,2.0). I want to construct a new variable bar using foo as a prototype with field y = 3.0 (or, alternatively non-destructively update foo producing a new Foo object). In ML languages (Haskell, OCaml, F#) and a few others (e.g. Clojure) there is an idiom that in pseudo-code would look like

bar = {foo with y = 3.0}

Is there something like this in Julia?

Upvotes: 4

Views: 349

Answers (2)

David P. Sanders
David P. Sanders

Reputation: 5325

There is also setindex (without the ! at the end) implemented in the FixedSizeArrays.jl package, which does this in an efficient way.

Upvotes: 0

one-more-minute
one-more-minute

Reputation: 1406

This is tricky. In Clojure this would work with a data structure, a dynamically typed immutable map, so we simply call the appropriate method to add/change a key. But when working with types we'll have to do some reflection to generate an appropriate new constructor for the type. Moreover, unlike Haskell or the various MLs, Julia isn't statically typed, so one does not simply look at an expression like {foo with y = 1} and work out what code should be generated to implement it.

Actually, we can build a Clojure-esque solution to this; since Julia provides enough reflection and dynamism that we can treat the type as a sort of immutable map. We can use fieldnames to get the list of "keys" in order (like [:x, :y]) and we can then use getfield(foo, :x) to get field values dynamically:

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)

However, there's a reason this is called with_slow. Because of the reflection it's going to be nowhere near as fast as a handwritten function like withy(foo::Foo, y) = Foo(foo.x, y, foo.z). If Foo is parametised (e.g. Foo{T} with y::T) then Julia will be able to infer that withy(foo, 1.) returns a Foo{Float64}, but won't be able to infer with_slow at all. As we know, this kills the crab performance.

The only way to make this as fast as ML and co is to generate code effectively equivalent to the handwritten version. As it happens, we can pull off that version as well!

# 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

The first section is a hack to produce a symbol-like object, f"foo", whose value is known within the type system. The generated function is like a macro that takes types as opposed to expressions; because it has access to Foo and the field names it can generate essentially the hand-optimised version of this code. You can also check that Julia is able to properly infer the output type, if you parametrise Foo:

@code_typed with(x, f"y" => 4., f"z" => "hello") # => ...::Foo{Int,Float64,String}

(The for nargs line is essentially a manually-unrolled reduce which enables this.)

Finally, lest I be accused of giving slightly crazy advice, I want to warn that this isn't all that idiomatic in Julia. While I can't give very specific advice without knowing your use case, it's generally best to have fields with a manageable (small) set of fields and a small set of functions which do the basic manipulation of those fields; you can build on those functions to create the final public API. If what you want is really an immutable dict, you're much better off just using a specialised data structure for that.

Upvotes: 4

Related Questions