Mutabilities.jl
Mutabilities — ModuleMutabilities: a type-level tool for ownership-by-convention
Mutabilities.jl is a type-level tool for describing mutabilities and ownership of objects in a composable manner.
See more in the documentation.
Summary
readonly: create read-only viewfreeze,freezevalue,freezeindex,freezeproperties: create immutable copiesmelt,meltvalue,meltindex,meltproperties: create mutable copiesmove!: manually elides copies with freeze/melt APIs.
High-level interface
Read-only view
The most easy-to-use interface is readonly(x) which creates a read-only "view" to x:
julia> using Mutabilities
julia> x = [1, 2, 3];
julia> z = readonly(x)
3-element readonly(::Array{Int64,1}) with eltype Int64:
1
2
3
julia> z[1] = 111
ERROR: setindex! not defined for Mutabilities.ReadOnlyArray{Int64,1,Array{Int64,1}}Note that changes in x would still be reflected to z:
julia> x[1] = 111;
julia> z
3-element readonly(::Array{Int64,1}) with eltype Int64:
111
2
3Freeze/melt
Use freeze(x) to get an independent immutable (shallow) copy of x:
julia> x = [1, 2, 3];
julia> z = freeze(x)
3-element freeze(::Array{Int64,1}) with eltype Int64:
1
2
3
julia> x[1] = 111;
julia> z
3-element freeze(::Array{Int64,1}) with eltype Int64:
1
2
3freeze can be reverted by melt:
julia> y = melt(z)
3-element Array{Int64,1}:
1
2
3It returns an independent mutable (shallow) copy of y. Thus, y can be safely mutated:
julia> y[1] = 111;
julia> z
3-element freeze(::Array{Int64,1}) with eltype Int64:
1
2
3Example usage
Julia's view is dangerous to use if the indices can be mutated after creating it:
idx = [1, 1, 1]
x = view([1], idx)
x[1] # OK
idx[1] = 10_000_000_000
x[1] # segfaultThis can be avoided by freezing the index array:
view([1], freeze(idx))Note that readonly is not enough.
Variants
freeze and melt work both on indices (keys) and values. It is possible to create an append-only vector by freezing the values:
julia> append_only = freezevalue([1, 2, 3]);
julia> push!(append_only, 4)
4-element freezevalue(::Array{Int64,1}) with eltype Int64:
1
2
3
4
julia> append_only[1] = 1
ERROR: setindex! not defined for Mutabilities.AppendOnlyVector{Int64,Array{Int64,1}}It is possible to create a shape-frozen vector by freezing the indices:
julia> shape_frozen = freezeindex([1, 2, 3])
3-element freezeindex(::Array{Int64,1}) with eltype Int64:
1
2
3
julia> shape_frozen .*= 10
3-element freezeindex(::Array{Int64,1}) with eltype Int64:
10
20
30
julia> push!(shape_frozen, 4)
ERROR: push! on freezeindex(::Array{Int64,1}) not allowedLow-level interface
Using freeze and melt at API boundaries is a good way to ensure correctness of the programs. However, until the julia compiler gets a borrow checker and automatically elides such copies, it may be very expensive to use them in some situations. Until then, Mutabilities.jl provides an "escape hatch"; i.e., an API to let the programmer declare that there is no sharing of the given object:
julia> z = freeze(move!([1, 2, 3])) # no copy
3-element freeze(::Array{Int64,1}) with eltype Int64:
1
2
3
julia> melt(move!(z)) # no copy
3-element Array{Int64,1}:
1
2
3This allows Julia programs to compose well, without defining immutable f and mutable f! variants of the API and without documenting the particular memory ownership for each function.
For example, melt is simply defined as
melt(x) = meltvalue(move!(meltindex(x)))move! can be useful when, e.g., input values can be re-used for output values:
julia> function add(x, y)
out = melt(x)
out .+= y
return freeze(move!(out))
end;
julia> x = ones(3)
y = ones(3);
julia> z = add(move!(x), y) # no allocations
3-element freeze(::Array{Float64,1}) with eltype Float64:
2.0
2.0
2.0
julia> melt(move!(z)) === x # `x` is mutated
true(Note: Above example intentionally violates the rule for using move! to show how it works. After add(move!(x), y), it is not allowed to use x, as done in the last statement.)
Supported collections and types
AbstractArrayAbstractDictAbstractSet- Data types ("plain
struct")
Interop
StaticArrays
Static arrays are converted to appropriate types instead of the wrapper arrays:
julia> using StaticArrays
julia> a = SA[1, 2, 3]
3-element SArray{Tuple{3},Int64,1,3} with indices SOneTo(3):
1
2
3
julia> melt(a)
3-element Array{Int64,1}:
1
2
3
julia> meltvalue(a)
3-element MArray{Tuple{3},Int64,1,3} with indices SOneTo(3):
1
2
3
julia> freeze(MVector(1, 2, 3)) # or freezevalue
3-element SArray{Tuple{3},Int64,1,3} with indices SOneTo(3):
1
2
3StructArrays
Mutabilities.jl is aware of mutability of each field arrays wrapped in struct arrays:
julia> using StructArrays
julia> x = StructArray(a = 1:3); # x.a is not mutable
julia> y = melt(x)
3-element StructArray(::Array{Int64,1}) with eltype NamedTuple{(:a,),Tuple{Int64}}:
(a = 1,)
(a = 2,)
(a = 3,)
julia> y.a
3-element Array{Int64,1}:
1
2
3
julia> z = freeze(StructArray(a = [1, 2, 3]))
3-element freeze(StructArray(::Array{Int64,1})) with eltype NamedTuple{(:a,),Tuple{Int64}}:
(a = 1,)
(a = 2,)
(a = 3,)
julia> z.a
3-element freeze(::Array{Int64,1}) with eltype Int64:
1
2
3Related packages
- https://github.com/andyferris/Freeze.jl
- https://github.com/bkamins/ReadOnlyArrays.jl
Mutabilities.readonly — Functionreadonly(x) -> zCreate a read-only view of x. Mutations on x are reflected to z.
Mutabilities.freeze — Functionmelt(z) -> x
meltvalue(z) -> x
meltindex(z) -> x
freeze(x) -> z
freezevalue(x) -> z
freezeindex(x) -> zmelt and meltvalue create a mutable copy of x. freeze, freezevalue, and freezeindex create an immutable copy of x.
The result of melt(::AbstractVector) is appendable. The result of meltvalue(::AbstractVector) may not be appendable. meltindex undoes freezeindex.
freezevalue only freezes the existing values and it is still possible to append the new items to z. freezeindex only freezes the indices and it is still possible to mutate the values.
Use, e.g., freeze(move!(x)) and melt(move!(z)) to freeze or melt the values without creating a copy (see also move!).
readonly can also be used to create a read-only view without creating a copy and without asserting strict absence of ownership.
Examples
julia> using Mutabilities
julia> z = freeze([1, 2, 3])
3-element freeze(::Array{Int64,1}) with eltype Int64:
1
2
3
julia> z[1] = 111
ERROR: setindex! not defined for Mutabilities.ImmutableArray{Int64,1,Array{Int64,1}}
julia> y = melt(z)
3-element Array{Int64,1}:
1
2
3Mutabilities.move! — Functionmove!(x)Manually declare that the object x has no other owners and the object x is not going to be used by the caller.
Examples
julia> using Mutabilities
julia> x = [];
julia> melt(move!(freeze(move!(x)))) === x
true
julia> melt(move!(freeze(x))) === x
false
julia> melt(freeze(move!(x))) === x
falseAbove examples intentionally violate the rule for using move! to show how it works. When x is passed to move! on the left hand side, it is not allowed to use x on the right hand side of ===.
Mutabilities.meltproperties — Functionmeltproperties(z) -> x
freezeproperties(x) -> zmeltproperties on an immutable data type (struct) object creates a mutable handle to it. This can be unwrapped using freezeproperties to obtain the "mutated" immutable object.
Examples
julia> using Mutabilities
julia> x = meltproperties(1 + 2im)
mutable handle to 1 + 2im
julia> x.re *= 100;
julia> x
mutable handle to 100 + 2im
julia> freezeproperties(x) :: Complex{Int}
100 + 2im
julia> x = meltproperties((a = 1, b = 2))
mutable handle to (a = 1, b = 2)
julia> x.a = 123;
julia> freezeproperties(x)
(a = 123, b = 2)