Is it possible to have "public/global" fields in Julia structs? - julia

In Julia it is possible to have public fields in functions for instance
function foo(arg)
global a = arg
a
end
Is it possible to achieve something similar using Julia structures.
For instance what I would like to do is:
julia> struct foobarfoo
global a
end
julia>
julia> test = foobarfoo(1)
ERROR: MethodError: no method matching foobarfoo(::Int64)
Stacktrace:
[1] top-level scope at none:0
julia> a
ERROR: UndefVarError: a not defined
Instead of:
julia> struct foobarfoo
a
end
julia> test = foobarfoo(1)
foobarfoo(1)
julia> test.a
1
julia>

I think that the short answer is no but you may be able to achieve what you want using the #unpack macro of Parameters.jl.

Related

Get struct property from inside struct function errors out

Recently started to try and learn Julia through examples. I am basically trying to figure out how to access a struct property from within a function inside the struct itself. E.g:
struct Test
a::Int
foo::Function
function Test()
return new(777, xfoo)
end
function xfoo()
println(a)
end
end
t = Test()
t.foo()
I get:
ERROR: LoadError: UndefVarError: a not defined
Stacktrace:
[1] (::var"#xfoo#1")()
# Main /tmp/j.jl:10
[2] top-level scope
# /tmp/j.jl:15
in expression starting at /tmp/j.jl:15
Am I using Julia wrong or am I missing something?
Julia is not object oriented language so object oriented patterns are usually not a good idea.
Hence xfoo should be outside of Test:
function xfoo(t::Test)
println(t.a)
end
There are packages that try to emulate OOP with Julia (however this is not a Julian pattern): https://github.com/Suzhou-Tongyuan/ObjectOriented.jl
You can also easily find quite a lot of discussion behind the design decision no to make Julia OOP. Start with: https://discourse.julialang.org/t/why-there-is-no-oop-object-oriented-programming-in-julia/86723
Workaround
Just out of curiosity one can find some workaround to attach a function to a struct (not a recommended design pattern!). For an example:
mutable struct MyTest
a::Int
foo::Function
function MyTest()
s = Ref{MyTest}()
s[] = new(777, () -> println(s[].a))
s[]
end
end
And some sample usage:
julia> t = MyTest();
julia> t.foo()
777
julia> t.a = 900;
julia> t.foo()
900

Purpose of "where" keyword in constructor of a parametric type

For the Julia manual parametric composite type example
struct Point{T}
x::T
y::T
end
it is possible to write an outer constructor such as
Point(x::T, y::T) where {T} = Point{T}(x, y)
Why is the where {T} part needed, i.e. why isn't
Point(x::T, y::T) = Point{T}(x, y)
possible? Julia complains that T is not defined, but could it not figure out that T is a type from the :: syntax? I am a newcomer to Julia so I might be missing something pretty basic.
T is a variable that has to be defined. If you do not define it in where Julia starts looking for it in an outer scope. Here is an example:
julia> struct Point{T}
x::T
y::T
end
julia> Point(x::T, y::T) = Point{T}(x, y)
ERROR: UndefVarError: T not defined
Stacktrace:
[1] top-level scope at REPL[2]:1
julia> T = Integer
Integer
julia> Point(x::T, y::T) = Point{T}(x, y)
Point
julia> Point(1,1)
Point{Integer}(1, 1)
Note that in this example you do not get what you expected to get, as probably you have hoped for Point{Int64}(1,1).
Now the most tricky part comes:
julia> Point(1.5,1.5)
Point{Float64}(1.5, 1.5)
What is going on you might ask. Well:
julia> methods(Point)
# 2 methods for type constructor:
[1] (::Type{Point})(x::Integer, y::Integer) in Main at REPL[4]:1
[2] (::Type{Point})(x::T, y::T) where T in Main at REPL[1]:2
The point is that The (::Type{Point})(x::T, y::T) where T already got defined by default when the struct as defined so with the second definition you have just added a new method for T = Integer.
In short - always use where to avoid getting surprising results. For example if you would write:
julia> T = String
String
julia> Point(1,1)
ERROR: MethodError: Cannot `convert` an object of type Int64 to an object of type String
you get a problem. As the signature of Point is fixed when it is defined but in the body T is taken from a global scope, and you have changed it from Integer to String, so you get an error.
Expanding on what #Oscar Smith noted the location of where is also sensitive to scope, and tells Julia on what level the "wildcard" is introduced. For example:
ulia> T1 = Vector{Vector{T} where T<:Real}
Array{Array{T,1} where T<:Real,1}
julia> T2 = Vector{Vector{T}} where T<:Real
Array{Array{T,1},1} where T<:Real
julia> isconcretetype(T1)
true
julia> isconcretetype(T2)
false
julia> T1 isa UnionAll
false
julia> T2 isa UnionAll
true
julia> x = T1()
0-element Array{Array{T,1} where T<:Real,1}
julia> push!(x, [1,2])
1-element Array{Array{T,1} where T<:Real,1}:
[1, 2]
julia> push!(x, [1.0, 2.0])
2-element Array{Array{T,1} where T<:Real,1}:
[1, 2]
[1.0, 2.0]
and you can see that T1 is a concrete type that can have an instance, and we created one calling it x. This concrete type can hold vectors whose element type is <:Real, but their element types do not have to be the same.
On the other hand T2 is a UnionAll, i.e. some of its "widlcards" are free (not known yet). However, the restriction is that in all concrete types that match T2 all vectors must have the same element type, so:
julia> Vector{Vector{Int}} <: T2
true
julia> Vector{Vector{Real}} <: T2
true
but
julia> T1 <: T2
false
In other words in T2 the wildcard must have one concrete value that can be matched by a concrete type.
I'm going to take a different tack from BogumiƂ's excellent answer (which is 100% correct, but might be missing the crux of the confusion).
There's nothing special about the name T. It's just a common convention for a short name for an arbitrary type. Kinda like how we often use the name A for matrices or i in for loops. The fact that you happened to use the same name in the struct Point{T} and in the outer constructor is irrelevant (but handy for the readers of your code). You could have just as well done:
struct Point{SpecializedType}
x::SpecializedType
y::SpecializedType
end
Point(x::Wildcard, y::Wildcard) where {Wildcard <: Any} = Point{Wildcard}(x, y)
That behaves exactly the same as what you wrote. Both of the above syntaxes (the struct and the method) introduce a new name that will behave like a "wildcard" that specializes and matches appropriately. When you don't have a where clause, you're no longer introducing a wildcard. Instead, you're just referencing types that have already been defined. For example:
Point(x::Int, y::Int) = Point{Int}(x, y)
That is, this will reference the Int that was already defined.
I suppose you could say, but if T wasn't defined, why can't Julia just figure out that it should be used as a wildcard. That may be true, but it introduces a little bit of non-locality to the syntax, where the behavior is drastically different depending upon what happens to be defined (or even exported from a used package).

Having some trouble with Julia's conversion rules

In the Julia documentation manual, it says the following [1]:
When is convert called?
The following language constructs call convert:
Assigning to an array converts to the array's element type.
[1] https://docs.julialang.org/en/v1/manual/conversion-and-promotion/#When-is-convert-called?-1
I've defined the following code:
julia> abstract type Element end
julia> abstract type Inline <: Element end
julia> struct Str <: Inline
content::String
end
julia> convert(::Type{Str}, e::String) = Str(e)
convert (generic function with 1 method)
julia> convert(::Type{Element}, e::String) = convert(Str, e)
convert (generic function with 2 methods)
I have convert defined for Julia type String. And converting to Element and converting to Str from an instance of type String works as expected. However, the following fails:
julia> convert(Str, "hi")
Str("hi")
julia> convert(Element, "hi")
Str("hi")
julia> arr = Element[]
0-element Array{Element,1}
julia> push!(arr, "hi")
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Element
Closest candidates are:
convert(::Type{T}, ::T) where T at essentials.jl:168
Stacktrace:
[1] push!(::Array{Element,1}, ::String) at ./array.jl:866
[2] top-level scope at REPL[25]:1
julia> arr = Str[]
0-element Array{Str,1}
julia> push!(arr, "hi")
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Str
Closest candidates are:
convert(::Type{T}, ::T) where T at essentials.jl:168
Str(::String) at REPL[19]:2
Str(::Any) at REPL[19]:2
Stacktrace:
[1] push!(::Array{Str,1}, ::String) at ./array.jl:866
[2] top-level scope at REPL[27]:1
julia>
Can someone explain why the above fails? And how, if it is possible to do so, prevent it from failing?
the main hint of your code was this line:
convert (generic function with 1 method)
that means that the convert function has only one definition (your definition), so something was wrong. when Julia uses convert, it actually calls Base.convert. so, overloading the proper method:
Base.convert(::Type{Str}, e::String) = Str(e)
Base.convert(::Type{Element}, e::String) = convert(Str, e)
worked as expected with your code.
In a nutshell, your code was right, you just were overloading the wrong convert (add always Base.method when working with Julia core functions)

Abstract Parametric Types in Julia

Is it possible in Julia to have two structs with the same name but be assigned different types and thus be distinguishable?
I have been reading https://docs.julialang.org/en/v1/manual/types/#Parametric-Types-1 and it seems to be leading towards what I want but I can't get it to work...
In force-fields for molecular simulation there are dihedral parameters to describe torsion angles in molecules. There are different kinds for example purposes lets limit them to 2 kinds: proper and improper. I would like to have two structures, both called dihedral, but given the types "proper" and "improper". I would then have methods specific to each type to calculate the forces due to dihedrals. I think abstract parametric types get me the closest to what I want but I can't get them sorted...
abstract type proper end
abstract type improper end
struct Dihedral <: proper
ai::Int64
kparam::Vector{Float64}
end
struct Dihedral <: improper
ai:Int64
kparam::Float64
end
The above code does not work... I have tried using
abstract type dihedral end
abstract type proper <: dihedral end
abstract type improper <: dihedral end
struct Dihedral <: dihedral{proper}
...
end
struct Dihedral <: dihedral{improper}
...
end
But I always get in trouble for redefining Dihedral
ERROR: LoadError: invalid redefinition of constant Dihedral
Stacktrace:
[1] top-level scope at none:0
My thought is that I can add in more types of dihedrals and all i need to do is also add in their methods and the simulation will automatically use the new dihedral.methods. If I try making structs of different names, then I start having to use if statements to direct the program to the correct structure and later to the correct methods... This is what I want to avoid i.e.,
if dihedraltype == "proper"
struct_proper(...)
elseif dihedraltype =="improper"
struct_improper()
elseif dihedraltype == "newStyle"
struct_newStyle()
end
using this method I would have to find all places in my code where I call dihedral and add in the new type... dihedral is just an example, there are many "phenomenas" that have different methods for calculating the phenomena.
I would use the following approach if you want to use a parametric type:
abstract type DihedralType end
struct Proper <: DihedralType
ai::Int64
kparam::Vector{Float64}
end
struct Improper <: DihedralType
ai::Int64
kparam::Float64
end
struct Dihedral{T<:DihedralType}
value::T
end
Dihedral(ai::Int64, kparam::Vector{Float64}) = Dihedral(Proper(ai, kparam))
Dihedral(ai::Int64, kparam::Float64) = Dihedral(Improper(ai, kparam))
and now you can write e.g.:
Dihedral(1, [1.0, 2.0])
Dihedral(1, 1.0)
The parameter of type Dihedral passes you information what kind of the object you are working with. Then some methods may be generic and call Dihedral e.g.:
julia> ai(d::Dihedral) = d.value.ai
ai (generic function with 1 method)
julia> ai(Dihedral(1, 1.0))
1
julia> ai(Dihedral(1, [1.0, 2.0]))
1
julia> kparam(d::Dihedral) = d.value.kparam
kparam (generic function with 1 method)
julia> kparam(Dihedral(1, 1.0))
1.0
julia> kparam(Dihedral(1, [1.0, 2.0]))
2-element Array{Float64,1}:
1.0
2.0
and some may be type parameter specific:
julia> len(d::Dihedral{Proper}) = length(kparam(d))
len (generic function with 1 method)
julia> len(Dihedral(1, [1.0, 2.0]))
2
julia> len(Dihedral(1, 1.0))
ERROR: MethodError: no method matching len(::Dihedral{Improper})
Closest candidates are:
len(::Dihedral{Proper}) at REPL[15]:1
Stacktrace:
[1] top-level scope at none:0
Does this approach give you what you have expected?
EDIT
Actually maybe an even simpler approach may be enough for you (depending on the use case). Just define:
abstract type AbstractDihedral end
struct Proper <: AbstractDihedral
ai::Int64
kparam::Vector{Float64}
end
struct Improper <: AbstractDihedral
ai::Int64
kparam::Float64
end
and then implement methods in terms of DihedralType if they are generic for all dihedrals and if you want to add some specific method to a given concrete type just add this method with this concrete type in the signature. For example:
ai(d::AbstractDihedral) = d.ai
kparam(d::AbstractDihedral) = d.kparam
len(d::Proper) = length(d.kparam) # will not work for Improper
In this approach you do not need to use a parametric type. The difference is that in the parametric type approach you can extract out the parameters that are the same for all dihedrals to the "parent" struct and define only dihedral specific parameters in the "wrapped" struct. In the second approach you have define all fields every time for each struct.

how to make Julia detect incompatible types on assignment?

Sorry for the basic question, googling was not too useful so far.
I am a newbie. I'd like to ask if it is possible to have Julia automatically detect incompatible types on assignments.
For example, when I write
julia> x=10;
julia> typeof(x)
Int32
julia> y=9.0;
julia> typeof(y)
Float64
julia> x=y // I'd like this to generate an error or a warning at least
9.0
julia> typeof(x) // do not want automatic type conversion
Float64
I found that if change the assignment to
julia> (x=y)::Int32
ERROR: type: typeassert: expected Int32, got Float64
But I do not want to write this all the time, and want Julia to automatically detect this.
I tried to do declarations like this below, but I seem to be doing something wrong.
julia> x::Int32=10;
julia> y::Float64=9.0
ERROR: type: typeassert: expected Float64, got Int32
For example, in Java, this generates a compile error:
public class App{
public static void main (String[] args) {
int x=10;
double y=9.0;
x=y;
}
}
error: incompatible types: possible lossy conversion from double to int x=y;
What do I need to change to make this happen? do I need to declare x,y with the correct type before? how? I am using Julia 0.3 on Linux.
Values represented by variables can be qualified to always have a particular type, and incompatible values will be checked, but the check for this violation occurs at run time. From the documentation:
When appended to a variable in a statement context, the :: operator means something a bit different: it declares the variable to always have the specified type, like a type declaration in a statically-typed language such as C. Every value assigned to the variable will be converted to the declared type using the convert function.
here is a important caveat, though
Currently, type declarations cannot be used in global scope, e.g. in the REPL, since Julia does not yet have constant-type globals.
So your experiment does not work because you're using globals. However, type assertions using variables scoped in a function do.
So taking your example
Case I
julia> main()=(x::Int64=10;y::Float64=9.0;x=y)
main (generic function with 1 method)
julia> main()
9.0
That looks like the wrong thing happened, but the result of a function is last expression evaluated. So in Case I, the assignment expression returns a Float64 value, though x is still implicitly assigned 9.0 converted to an Int64 or 9. But in Case II, the last expression is simply x which is Int64.
Case II
julia> main()=(x::Int64=10;y::Float64=9.0;x=y;x)
main (generic function with 1 method)
julia> main()
9
julia> typeof(main())
Int64
When y takes on a value that can't be converted without loss to Int64 an error is thrown
Case III
julia> main()=(x::Int64=10;y::Float64=9.9;x=y;x)
main (generic function with 1 method)
julia> main()
ERROR: InexactError()
in main at none:1
The Guts
You can use the function code_typed to see what is going to more precisely. Consider the following:
julia> f(y::Float64)=(x::Int64=y)
f (generic function with 1 method)
julia> code_typed(f,(Float64,))
1-element Array{Any,1}:
:($(Expr(:lambda, {:y}, {{:x},{{:y,Float64,0},{:x,Int64,18}},{}}, :(begin # none, line 1:
x = top(typeassert)(top(box)(Int64,top(checked_fptosi)(Int64,y::Float64))::Int64,Int64)::Int64
return y::Float64
end::Float64))))
julia> f(y::Float64)=(x::Int64=y;x)
f (generic function with 1 method)
julia> code_typed(f,(Float64,))
1-element Array{Any,1}:
:($(Expr(:lambda, {:y}, {{:x},{{:y,Float64,0},{:x,Int64,18}},{}}, :(begin # none, line 1:
x = top(typeassert)(top(box)(Int64,top(checked_fptosi)(Int64,y::Float64))::Int64,Int64)::Int64
return x::Int64
end::Int64))))

Resources