I have recently started to do object-oriented stuff in Julia and have enjoyed using structs (the Python equivalent to a class?).
I read the documentation about structs here, but I did not see anything about constructors or methods in the struct. I have seen structs used a few places where someone actually defines a function inside of the struct. How can I do that and why would you want to do that?
When you finish reading carefully the documentation suggested by StefanKarpinski (and it is really good), have a look for the Parameters package which, in my opinion, makes the object-oriented transformation to Julia much nicer.
Have a look at this code:
#with_kw mutable struct Agent
age = 0
income = rand()
end
function Agent(age::Int)
income = rand()+age*10
Agent(age, income)
end
And now some usages:
julia> Agent()
Agent
age: Int64 0
income: Float64 0.28109332504865625
julia> Agent(income=33)
Agent
age: Int64 0
income: Int64 33
julia> Agent(age=3)
Agent
age: Int64 3
income: Float64 0.5707873066917069
julia> Agent(30)
Agent
age: Int64 30
income: Float64 300.1706559468855
The last constructor was the custom-made one, while the three previous were automatically generated by the #withkw macro.
Last, but not least. Consider a loop reference data structure, i.e., where AgentY1 can only have friends of type AgentY2 and AgentY2 can only have friends of type AgentY1:
To my best knowledge (maybe someone can correct me if I am wrong) this can be only achieved using the macro:
#with_kw mutable struct AgentY1
age::Int
friends=AgentY2[]
end
#with_kw mutable struct AgentY2
age::Int
friends=AgentY1[]
end
And now sample usage:
julia> aa = AgentY1(age=11)
AgentY1
age: Int64 11
friends: Array{AgentY2}((0,))
This is how I do my OO programming in Julia.
Related
Are the numerator and denominator stored as integers? Specifically, how are implemented the basic operations: sum, subtraction, multiplication, division?
https://github.com/JuliaLang/julia/blob/248bbf6d61b643d0101bf96093cd7621e5bcf477/base/rational.jl#L9-L15
the code is fairly readable, in short:
struct Rational{T<:Integer} <: Real
means that both numerator and denominator have the same type, and that type is <: Integer.
if you want to know how some operations are implemented, try running this in your REPL:
julia> #edit 1//2 + 3//4
it should bring you to https://github.com/JuliaLang/julia/blob/248bbf6d61b643d0101bf96093cd7621e5bcf477/base/rational.jl#L285
Even a faster and easier way to understand what is going on in Julia is just to use the dump command such as:
julia> dump(3//4)
Rational{Int64}
num: Int64 3
den: Int64 4
I try to understand typing in Julia and encounter the following problem with Array. I wrote a function bloch_vector_2d(Array{Complex,2}); the detailed implementation is irrelevant. When calling, here is the complaint:
julia> bloch_vector_2d(rhoA)
ERROR: MethodError: no method matching bloch_vector_2d(::Array{Complex{Float64},2})
Closest candidates are:
bloch_vector_2d(::Array{Complex,2}) at REPL[56]:2
bloch_vector_2d(::StateAB) at REPL[54]:1
Stacktrace:
[1] top-level scope at REPL[64]:1
The problem is that an array of parent type is not automatically a parent of an array of child type.
julia> Complex{Float64} <: Complex
true
julia> Array{Complex{Float64},2} <: Array{Complex,2}
false
I think it would make sense to impose in julia that Array{Complex{Float64},2} <: Array{Complex,2}. Or what is the right way to implement this in Julia? Any helps or comments are appreciated!
This issue is discussed in detail in the Julia Manual here.
Quoting the relevant part of it:
In other words, in the parlance of type theory, Julia's type parameters are invariant, rather than being covariant (or even contravariant). This is for practical reasons: while any instance of Point{Float64} may conceptually be like an instance of Point{Real} as well, the two types have different representations in memory:
An instance of Point{Float64} can be represented compactly and efficiently as an immediate pair of 64-bit values;
An instance of Point{Real} must be able to hold any pair of instances of Real. Since objects that are instances of Real can be of arbitrary size and structure, in practice an instance of Point{Real} must be represented as a pair of pointers to individually allocated Real objects.
Now going back to your question how to write a method signature then you have:
julia> Array{Complex{Float64},2} <: Array{<:Complex,2}
true
Note the difference:
Array{<:Complex,2} represents a union of all types that are 2D arrays whose eltype is a subtype of Complex (i.e. no array will have this exact type).
Array{Complex,2} is a type that an array can have and this type means that you can store Complex values in it that can have mixed parameter.
Here is an example:
julia> x = Complex[im 1im;
1.0im Float16(1)im]
2×2 Array{Complex,2}:
im 0+1im
0.0+1.0im 0.0+1.0im
julia> typeof.(x)
2×2 Array{DataType,2}:
Complex{Bool} Complex{Int64}
Complex{Float64} Complex{Float16}
Also note that the notation Array{<:Complex,2} is the same as writing Array{T,2} where T<:Complex (or more compactly Matrix{T} where T<:Complex).
This is more of a comment, but I can't hesitate posting it. This question apprars so often. I'll tell you why that phenomenon must arise.
A Bag{Apple} is a Bag{Fruit}, right? Because, when I have a JuicePress{Fruit}, I can give it a Bag{Apple} to make some juice, because Apples are Fruits.
But now we run into a problem: my fruit juice factory, in which I process different fruits, has a failure. I order a new JuicePress{Fruit}. Now, I unfortunately get delivered a replacement JuicePress{Lemon} -- but Lemons are Fruits, so surely a JuicePress{Lemon} is a JuicePress{Fruit}, right?
However, the next day, I feed apples to the new press, and the machine explodes. I hope you see why: JuicePress{Lemon} is not a JuicePress{Fruit}. On the contrary: a JuicePress{Fruit} is a JuicePress{Lemon} -- I can press lemons with a fruit-agnostic press! They could have sent me a JuicePress{Plant}, though, since Fruits are Plants.
Now we can get more abstract. The real reason is: function input arguments are contravariant, while function output arguments are covariant (in an idealized setting)2. That is, when we have
f : A -> B
then I can pass in supertypes of A, and end up with subtypes of B. Hence, when we fix the first argument, the induced function
(Tree -> Apple) <: (Tree -> Fruit)
whenever Apple <: Fruit -- this is the covariant case, it preserves the direction of <:. But when we fix the second one,
(Fruit -> Juice) <: (Apple -> Juice)
whenever Fruit >: Apple -- this inverts the diretion of <:, and therefore is called contravariant.
This carries over to other parametric data types, since there, too, you usually have "output-like" parameters (as in the Bag), and "input-like" parameters (as with the JuicePress). There can also be parameters that behave like neither (e.g., when they occur in both fashions) -- these are then called invariant.
There are now two ways in which languages with parametric types solve this problem. The, in my opinion, more elegant one is to mark every parameter: no annotation means invariant, + means covariant, - means contravariant (this has technical reasons -- those parameters are said to occur in "positive" and "negative position"). So we had the Bag[+T <: Fruit], or the JuicePress[-T <: Fruit] (should be Scala syntax, but I haven't tried it). This makes subtyping more complicated, though.
The other route to go is what Julia does (and, BTW, Java): all types are invariant1, but you can specify upper and lower unions at the call site. So you have to say
makejuice(::JoicePress{>:T}, ::Bag{<:T}) where {T}
And that's how we arrive at the other answers.
1Except for tuples, but that's weird.
2This terminology comes from category theory. The Hom-functor is contravariant in the first, and covariant in the second argument. There's an intuitive realization of subtyping through the "forgetful" functor from the category Typ to the poset of Types under the <: relation. And the CT terminology in turn comes from tensors.
While the "how it works" discussion has been done in the another answer, the best way to implement your method is the following:
function bloch_vector_2d(a::AbstractArray{Complex{T}}) where T<:Real
sum(a) + 5*one(T) # returning something to see how this is working
end
Now this will work like this:
julia> bloch_vector_2d(ones(Complex{Float64},4,3))
17.0 + 0.0im
I try to understand typing in Julia and encounter the following problem with Array. I wrote a function bloch_vector_2d(Array{Complex,2}); the detailed implementation is irrelevant. When calling, here is the complaint:
julia> bloch_vector_2d(rhoA)
ERROR: MethodError: no method matching bloch_vector_2d(::Array{Complex{Float64},2})
Closest candidates are:
bloch_vector_2d(::Array{Complex,2}) at REPL[56]:2
bloch_vector_2d(::StateAB) at REPL[54]:1
Stacktrace:
[1] top-level scope at REPL[64]:1
The problem is that an array of parent type is not automatically a parent of an array of child type.
julia> Complex{Float64} <: Complex
true
julia> Array{Complex{Float64},2} <: Array{Complex,2}
false
I think it would make sense to impose in julia that Array{Complex{Float64},2} <: Array{Complex,2}. Or what is the right way to implement this in Julia? Any helps or comments are appreciated!
This issue is discussed in detail in the Julia Manual here.
Quoting the relevant part of it:
In other words, in the parlance of type theory, Julia's type parameters are invariant, rather than being covariant (or even contravariant). This is for practical reasons: while any instance of Point{Float64} may conceptually be like an instance of Point{Real} as well, the two types have different representations in memory:
An instance of Point{Float64} can be represented compactly and efficiently as an immediate pair of 64-bit values;
An instance of Point{Real} must be able to hold any pair of instances of Real. Since objects that are instances of Real can be of arbitrary size and structure, in practice an instance of Point{Real} must be represented as a pair of pointers to individually allocated Real objects.
Now going back to your question how to write a method signature then you have:
julia> Array{Complex{Float64},2} <: Array{<:Complex,2}
true
Note the difference:
Array{<:Complex,2} represents a union of all types that are 2D arrays whose eltype is a subtype of Complex (i.e. no array will have this exact type).
Array{Complex,2} is a type that an array can have and this type means that you can store Complex values in it that can have mixed parameter.
Here is an example:
julia> x = Complex[im 1im;
1.0im Float16(1)im]
2×2 Array{Complex,2}:
im 0+1im
0.0+1.0im 0.0+1.0im
julia> typeof.(x)
2×2 Array{DataType,2}:
Complex{Bool} Complex{Int64}
Complex{Float64} Complex{Float16}
Also note that the notation Array{<:Complex,2} is the same as writing Array{T,2} where T<:Complex (or more compactly Matrix{T} where T<:Complex).
This is more of a comment, but I can't hesitate posting it. This question apprars so often. I'll tell you why that phenomenon must arise.
A Bag{Apple} is a Bag{Fruit}, right? Because, when I have a JuicePress{Fruit}, I can give it a Bag{Apple} to make some juice, because Apples are Fruits.
But now we run into a problem: my fruit juice factory, in which I process different fruits, has a failure. I order a new JuicePress{Fruit}. Now, I unfortunately get delivered a replacement JuicePress{Lemon} -- but Lemons are Fruits, so surely a JuicePress{Lemon} is a JuicePress{Fruit}, right?
However, the next day, I feed apples to the new press, and the machine explodes. I hope you see why: JuicePress{Lemon} is not a JuicePress{Fruit}. On the contrary: a JuicePress{Fruit} is a JuicePress{Lemon} -- I can press lemons with a fruit-agnostic press! They could have sent me a JuicePress{Plant}, though, since Fruits are Plants.
Now we can get more abstract. The real reason is: function input arguments are contravariant, while function output arguments are covariant (in an idealized setting)2. That is, when we have
f : A -> B
then I can pass in supertypes of A, and end up with subtypes of B. Hence, when we fix the first argument, the induced function
(Tree -> Apple) <: (Tree -> Fruit)
whenever Apple <: Fruit -- this is the covariant case, it preserves the direction of <:. But when we fix the second one,
(Fruit -> Juice) <: (Apple -> Juice)
whenever Fruit >: Apple -- this inverts the diretion of <:, and therefore is called contravariant.
This carries over to other parametric data types, since there, too, you usually have "output-like" parameters (as in the Bag), and "input-like" parameters (as with the JuicePress). There can also be parameters that behave like neither (e.g., when they occur in both fashions) -- these are then called invariant.
There are now two ways in which languages with parametric types solve this problem. The, in my opinion, more elegant one is to mark every parameter: no annotation means invariant, + means covariant, - means contravariant (this has technical reasons -- those parameters are said to occur in "positive" and "negative position"). So we had the Bag[+T <: Fruit], or the JuicePress[-T <: Fruit] (should be Scala syntax, but I haven't tried it). This makes subtyping more complicated, though.
The other route to go is what Julia does (and, BTW, Java): all types are invariant1, but you can specify upper and lower unions at the call site. So you have to say
makejuice(::JoicePress{>:T}, ::Bag{<:T}) where {T}
And that's how we arrive at the other answers.
1Except for tuples, but that's weird.
2This terminology comes from category theory. The Hom-functor is contravariant in the first, and covariant in the second argument. There's an intuitive realization of subtyping through the "forgetful" functor from the category Typ to the poset of Types under the <: relation. And the CT terminology in turn comes from tensors.
While the "how it works" discussion has been done in the another answer, the best way to implement your method is the following:
function bloch_vector_2d(a::AbstractArray{Complex{T}}) where T<:Real
sum(a) + 5*one(T) # returning something to see how this is working
end
Now this will work like this:
julia> bloch_vector_2d(ones(Complex{Float64},4,3))
17.0 + 0.0im
I am currently working on a parser which supports binary operations. The idea for representing a binary operation is to use a structure of this sort
struct Binary{T <: BinaryKind} <: Operation
xs::Vector{Union{Operation, Atom}}
end
Where both Operation and Atom are abstract types. After reading the performance tips in the julia documentation, I have come to realize that a more efficient way of representing this structure would be
struct Binary{T <: BinaryKind, D <: Tuple} <: Operation
xs::D
end
But Since I can have nested binary operations, I believe that there could be cases in which I would end up with very long type definitions, which could be even worse than just using an abstract type. Is there a way I can improve this?
I think (too long for the comment so I give it as an answer) that in this case it is probably better to be type unstable. Note that this is exactly what Julia does itself:
julia> x = :(1+2*(3+4))
:(1 + 2 * (3 + 4))
julia> dump(x)
Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol +
2: Int64 1
3: Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol *
2: Int64 2
3: Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol +
2: Int64 3
3: Int64 4
Of course Julia has a significantly more rich syntax but even in your simple case consider the following. You get the benefits of type stability if you compile some part of code once and then run it many times (in this or other form).
Now I assume that what you write would be evaluated in practice mostly only one time. If you made your expressions fully type stable you have to pay each time (assuming that the expression changes):
the cost of compiling it (expensive)
the cost of running it (relatively cheap)
If your code would be type unstable you have to pay cost of compilation only once. It is true that running it will be a bit slower, but overall probably it will be better to do this this way.
On the other hand - if you expect that you define the expression only once and then run it many times, then probably a better approach is to use metaprogramming:
process your expression only once and generate Julia code that will be evaluating your expression
then Julia will compile the generated code once
you get a maximum performance of executing it after the steps 1 and 2 are made
A half-measure to your question would be to use the following data structure:
struct Binary{T <: BinaryKind, S} <: Operation
xs::Vector{S}
end
In this way your code will be type-stable if S is a concrete type or a small union of concrete types, and type unstable otherwise (and I expect that then you can try to make the rest of the code to generate xs in a way that its eltype is concrete or a small union of concrete types).
(if you have more questions on this please comment and I can expand the answer)
I'm trying to understand the structure of arrays in the Julia Type Graph. This seems very counter-intuitive to me:
julia> Int64 <: Number
true
julia> Array{Int64,1} <: Array{Number,1}
false
julia> Array{Int64,1} <: Array{Int,1}
true
It seems that a <: b is not sufficient for Array{a,1} <: Array{b,1}. When does Array{a,1} <: Array{b,1}?
A practical corollary: how can I type-declare an abstract array of numbers?
In the following page of the manual, it's described how julia's types are invariant as opposed to covariant.
https://docs.julialang.org/en/v1/manual/types/#Parametric-Composite-Types-1
See in particular the warning admonition stating
This last point is very important: even though Float64 <: Real we DO NOT have Point{Float64} <: Point{Real}.
And the following explanation given
In other words, in the parlance of type theory, Julia's type parameters are invariant, rather than being covariant (or even contravariant). This is for practical reasons: while any instance of Point{Float64} may conceptually be like an instance of Point{Real} as well, the two types have different representations in memory:
An instance of Point{Float64} can be represented compactly and efficiently as an immediate pair of 64-bit values;
An instance of Point{Real} must be able to hold any pair of instances of Real. Since objects that are instances of Real can be of arbitrary size and structure, in practice an instance of Point{Real} must be represented as a pair of pointers to individually allocated Real objects.
An abstract array with any kind of number is denoted like this
AbstractArray{<:Number} which is short for AbstractArray{T} where T <: Number