Constrain the range of a parametric type to integer values - julia

I've created a parametrized type where the parameter is an integer. Below is a reasonable MWE.
struct InverseTaylor{N}
a::Vector{Float64}
function InverseTaylor{N}() where {N}
a = something_done_with_n(N)
new(a)
end
end
But I'm unhappy to have the where {N} not constraining the N further.
What I could think of during writing this question is to add a verification in the inner constructor, like so:
N isa Int || error("$N should be an integer")
Yet it still feels like there should be a better way.
P.S. I know there are caveats to using types parameterized on integer values, but I feel like if the language allows for the concept, there should also be a way to make it clear what I expect from N in the struct definition.

would
struct InverseTaylor{N}
a::Vector{Float64}
function InverseTaylor{N}() where N <: Integer
a = something_done_with_n(N)
new(a)
end
end
work for what you want to do?

Related

Can a julia struct be defined with persistent requirements on field dimensions?

If I define a new struct as
mutable struct myStruct
data::AbstractMatrix
labels::Vector{String}
end
and I want to throw an error if the length of labels is not equal to the number of columns of data, I know that I can write a constructor that enforces this condition like
myStruct(data, labels) = length(labels) != size(data)[2] ? error("Labels incorrect length") : new(data,labels)
However, once the struct is initialized, the labels field can be set to the incorrect length:
m = myStruct(randn(2,2), ["a", "b"])
m.labels = ["a"]
Is there a way to throw an error if the labels field is ever set to length not equal to the number of columns in data?
You could use StaticArrays.jl to fix the matrix and vector's sizes to begin with:
using StaticArrays
mutable struct MatVec{R, C, RC, VT, MT}
data::MMatrix{R, C, MT, RC} # RC should be R*C
labels::MVector{C, VT}
end
but there's the downside of having to compile for every concrete type with a unique permutation of type parameters R,C,MT,VT. StaticArrays also does not scale as well as normal Arrays.
If you don't restrict dimensions in the type parameters (with all those downsides) and want to throw an error at runtime, you got good and bad news.
The good news is you can control whatever mutation happens to your type. m.labels = v would call the method setproperty!(object::myStruct, name::Symbol, v), which you can define with all the safeguards you like.
The bad news is that you can't control mutation to the fields' types. push!(m.labels, 1) mutates in the push!(a::Vector{T}, item) method. The myStruct instance itself doesn't actually change; it still points to the same Vector. If you can't guarantee that you won't do something like x = m.labels; push!(x, "whoops") , then you really do need runtime checks, like iscorrect(m::myStruct) = length(m.labels) == size(m.data)[2]
A good option is to not access the fields of your struct directly. Instead, do it using a function. Eg:
mutable struct MyStruct
data::AbstractMatrix
labels::Vector{String}
end
function modify_labels(s::MyStruct, new_labels::Vector{String})
# do all checks and modifications
end
You should check chapter 8 from "Hands-On Design Patterns and Best Practices with Julia: Proven solutions to common problems in software design for Julia 1.x"

How to achieve type stability when assigning values with StaticArrays?

I have the following struct (simplified), and some calculations done with this struct:
mutable struct XX{VecType}
v::VecType
end
long_calculation(x::XX) = sum(x.v)
as a part of the program i need to update the v value. the struct is callable and mainly used as a cache. here, the use of static arrays helps a lot in speeding up calculations, but the type of v is ultimately defined by an user. my problem lies when assigning new values to XX.v:
function (f::XX)(w)
f.v .= w #here lies the problem
return long_calculation(f)
this works if v <: Array and w is of any value, but it doesn't work when v <: StaticArrays.StaticArray, as setindex! is not defined on that type.
How can i write f.v .= w in a way that, when v allows it, performs an inplace modification, but when not, just creates a new value, and stores it in the XX struct?
There's a package for exactly this use case: BangBang.jl. From there, you can use setindex!!:
f.v = setindex!!(f.v, w)
Here I propose a simple solution that should be enough in most cases. Use multiple dispatch and define the following function:
my_assign!(f::XX, w) = (f.v .= w)
my_assign!(f::XX{<:StaticArray}, w) = (f.v = w)
and then simply call it in your code like this:
function (f::XX)(w)
my_assign!(f, w)
return long_calculation(f)
end
Then if you (or your users) get an error with a default implementation it is easy enough to add another method to my_assign! co cover other special cases when it throws an error.
Would such a solution be enough for you?

Julia: Defining promote_rule with multiple parametric types

Say I want to define a promote_rule() for a type that has multiple parametric types, for example for type MyType:
abstract type State end
struct Open<:State end
struct Closed<:State end
struct MyType{T,S<:State}
x::T
state::S
end
Is there a way to define a promote_rule() which only promotes the first type and not the second, for example:
myFloat = MyType(1.0, Open()) # MyType{Float64, Open}
myInt = MyType(2, Closed()) # MyType{Int64, Closed}
promote(myFloat, myInt)
# (MyType{Float64, Open}, MyType{Float64, Closed})
By definition, the result of a promotion is one common type. So while you can just recursively promote the Ts, you have to resort to a common supertype for the Ss if you want to keep them as is. Simply using State would be a valid choice, but a Union leads to a bit more fine-grained results:
julia> Base.promote_rule(::Type{MyType{T1, S1}}, ::Type{MyType{T2, S2}}) where {T1, T2, S1, S2} = MyType{promote_type(T1, T2), <:Union{S1, S2}}
julia> promote_type(MyType{Int, Closed}, MyType{Float64, Closed})
MyType{Float64,#s12} where #s12<:Closed
julia> promote_type(MyType{Int, Closed}, MyType{Float64, Open})
MyType{Float64,#s12} where #s12<:Union{Closed, Open}
You still have to define the respective convert methods for promote to work, of course; specifically, one ignoring the state type:
julia> Base.convert(::Type{<:MyType{T}}, m::MyType) where {T} = MyType(convert(T, m.x), m.state)
julia> promote(myFloat, myInt)
(MyType{Float64,Open}(1.0, Open()), MyType{Float64,Closed}(2.0, Closed()))
But be sure to test all kinds of combinations really well. Promotion and conversion is really fiddly and hard to get right the first time, in my experience.

How can I shorten a type in the body of a function

Best be explained by an example:
I define a type
type myType
α::Float64
β::Float64
end
z = myType( 1., 2. )
Then suppose that I want to pass this type as an argument to a function:
myFunc( x::Vector{Float64}, m::myType ) =
x[1].^m.α+x[2].^m.β
Is there a way to pass myType so that I can actually use it in the body of the function in a "cleaner" fashion as follows:
x[1].^α+x[2].^β
Thanks for any answer.
One way is to use dispatch to a more general function:
myFunc( x::Vector{Float64}, α::Float64, β::Float64) = x[1].^α+x[2].^β
myFunc( x::Vector{Float64}, m::myType = myFunc(x,m.α,m.β)
Or if your functions are longer, you may want to use Parameters.jl's #unpack:
function myFunc( x::Vector{Float64}, m::myType )
#unpack m: α,β #now those are defined
x[1].^α+x[2].^β
end
The overhead of unpacking is small because you're not copying, it's just doing a bunch of α=m.α which is just making an α which points to m.α. For longer equations, this can be a much nicer form if you have many fields and use them in long calculations (for reference, I use this a lot in DifferentialEquations.jl).
Edit
There's another way as noted in the comments. Let me show this. You can define your type (with optional kwargs) using the #with_kw macro from Parameters.jl. For example:
using Parameters
#with_kw type myType
α::Float64 = 1.0 # Give a default value
β::Float64 = 2.0
end
z = myType() # Generate with the default values
Then you can use the #unpack_myType macro which is automatically made by the #with_kw macro:
function myFunc( x::Vector{Float64}, m::myType )
#unpack_myType m
x[1].^α+x[2].^β
end
Again, this only has the overhead of making the references α and β without copying, so it's pretty lightweight.
You could add this to the body of your function:
(α::Float64, β::Float64) = (m.α, m.β)
UPDATE: My original answer was wrong for a subtle reason, but I thought it was a very interesting bit of information so rather than delete it altogether, I'm leaving it with an explanation on why it's wrong. Many thanks to Fengyang for pointing out the global scope of eval! (as well as the use of $ in an Expr context!)
The original answer suggested that:
[eval( parse( string( i,"=",getfield( m,i)))) for i in fieldnames( m)]
would return a list comprehension which had assignment side-effects, since it conceptually would result in something like [α=1., β=2., etc]. The assumption was that this assignment would be within local scope. However, as pointed out, eval is always assessed at global scope, therefore the above one-liner does not do what it's meant to. Example:
julia> type MyType
α::Float64
β::Float64
end
julia> function myFunc!(x::Vector{Float64}, m::MyType)
α=5.; β=6.;
[eval( parse( string( i,"=",getfield( m,i)))) for i in fieldnames( m)]
x[1] = α; x[2] = β; return x
end;
julia> myFunc!([0.,0.],MyType(1., 2.))
2-element Array{Float64,1}:
5.0
6.0
julia> whos()
MyType 124 bytes DataType
myFunc 0 bytes #myFunc
α 8 bytes Float64
β 8 bytes Float64
I.e. as you can see, the intention was for the local variables α and β to be overwritten, but they didn't; eval placed α and β variables at global scope instead. As a matlab programmer I naively assumed that eval() was conceptually equivalent to Matlab, without actually checking. Turns out it's more similar to the evalin('base',...) command.
Thanks again to Fengyand for giving another example of why the phrase "parse and eval" seems to have about the same effect on Julia programmers as the word "it" on the knights who until recently said "NI". :)

Julia: why must parametric types have outer constructors?

The following works:
type TypeA
x :: Array
y :: Int
TypeA(x :: Array ) = new(x, 2)
end
julia> y = TypeA([1,2,3])
TypeA([1,2,3],2)
This does not:
type TypeB{S}
x :: Array{S}
y :: Int
TypeB{S}( x:: Array{S} ) = new(x,2)
end
julia> y = TypeB([1,2,3])
ERROR: `TypeB{S}` has no method matching TypeB{S}(::Array{Int64,1})
In order to get the second case to work, one has to declare the constructor outside of the type declaration. This is slightly undesirable.
My question is why this problem exists from a Julia-design standpoint so I can better reason about the Julia type-system.
Thank you.
This works:
type TypeB{S}
x::Array{S}
y::Int
TypeB(x::Array{S}) = new(x,2)
end
TypeB{Int}([1,2,3])
which I figured out by reading the manual, but I must admit I don't really understand inner constructors that well, especially for parametric types. I think its because you are actually defining a family of types, so the inner constructor is only sensible for each individual type - hence you need to specify the {Int} to say which type you want. You can add an outer constructor to make it easier, i.e.
type TypeB{S}
x::Array{S}
y::Int
TypeB(x::Array{S}) = new(x,2)
end
TypeB{S}(x::Array{S}) = TypeB{S}(x)
TypeB([1,2,3])
I think it'd be good to bring it up on the Julia issues page, because I feel like this outer helper constructor could be provided by default.
EDIT: This Julia issue points out the problems with providing an outer constructor by default.

Resources