I like immutables, I really do, they're simple to work with, fast, don't cause a…llocations. Really the only thing that bothers me about them is that they're well, immutable, making them a bit of pain to work with, esp. when wanting to construct one incrementally. So here's an attempt to remedy that.
Example usage:
```
julia> struct Foo
a::Int
end
julia> x = Foo(1)
Foo(1)
julia> x@a = 2
Foo(2)
julia> x
Foo(2)
```
The ways this works is that under the hood, it creates a new immutable object with the specified field modified and then assigns it back to the appropriate place. Syntax wise, everything to the left of the `@` is what's being assigned to, everything to the right of the `@` is what is to be modified. E.g.
```
struct Foo4; d::Int; end
struct Foo3; c::Foo4; end
mutable struct Foo2; b::Foo3; end
struct Foo1; a::Foo2; end
x = Foo1(Foo2(Foo3(Foo4(1))))
x.a.b@c.d = 2
julia> x
Foo1(Foo2(Foo3(Foo4(2))))
```
Internally, everything to the right of the `@` gets lowered to `setindex` (no bang) and `setfield` (also no bang), which are overridable by the user. The intent is to disallow `setfield` for immutables that have a non-default inner constructor, allowing the user to provide their own which checks any required invariants. That part isn't implemented here yet, however.
Lastly, LLVM isn't currently too happy about the IR this generates, so I'm working on making that happen to make sure this actually performs ok. I think from the julia side, this is pretty much the extent of it though. Pretty much untested at the moment. I want to get the LLVM side of things done first. With that in mind, feel free to check out this branch and see if you like it.
One motivating example here is of course efficient, generic fixed size arrays, so here's an example of that:
```
mutable struct MArray{Size, T, D, L} <: AbstractArray{T, D}
data::NTuple{L, T}
end
Base.size(::MArray{Size}) where {Size} = tuple(Size.parameters...)
@inline to_linear_index(::Type{Tuple{}}, idxs, stride) = 1
@inline to_linear_index(::Type, idxs::Tuple{}, stride) = 1
@inline to_linear_index(::Type{Tuple{A}}, idxs, stride) where {A} = idxs[1]
@inline to_linear_index(::Type{Tuple{A,B}}, idxs, stride) where {A, B} = A*(idxs[1]-1) + idxs[2]
@inline to_linear_index(T::Type{<:Tuple{A, Vararg{Any}}}, idxs, stride) where A =
(stride * (idxs[1]-1)) + to_linear_index(Base.tuple_type_tail(T), idxs[2:end], stride * A)
function Base.setindex!(a::MArray{Size}, v, idxs...) where Size
@inbounds a.data@[to_linear_index(Size, idxs, 1)] = v
v
end
Base.getindex(a::MArray{Size}, idxs...) where {Size} = a.data[to_linear_index(Size, idxs, 1)]
a = MArray{Tuple{2, 2}, Int, 2, 4}((1,2,3,4))
b = [1 2; 3 4]
c = [4 5; 6 7]
# Look ma, no allocations
julia> @benchmark A_mul_B!(a, b, c)
BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 86.193 ns (0.00% GC)
median time: 86.993 ns (0.00% GC)
mean time: 87.978 ns (0.00% GC)
maximum time: 3.282 μs (0.00% GC)
--------------
samples: 10000
evals/sample: 961
```
Fixes #11902
Supersedes #12113