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
3
Freeze/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
3
freeze
can be reverted by melt
:
julia> y = melt(z)
3-element Array{Int64,1}:
1
2
3
It 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
3
Example 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] # segfault
This 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 allowed
Low-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
3
This 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
AbstractArray
AbstractDict
AbstractSet
- 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
3
StructArrays
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
3
Related packages
- https://github.com/andyferris/Freeze.jl
- https://github.com/bkamins/ReadOnlyArrays.jl
Mutabilities.readonly
— Functionreadonly(x) -> z
Create 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) -> z
melt
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
3
Mutabilities.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
false
Above 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) -> z
meltproperties
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)