Intialize types and define operators within them

Dear all, I have two questions concerning composite types:

  1. Is it possible to set a variable to a type without initializing all its values? For example
type test
    a :: Int64
    b :: Float64
end
x = test()

(that does not work - and coudn’t find the answer in the manuals, I suppose that
is possible, or at least set the variable with default values).

  1. Is it possible to define operators acting on types, such as custom “<”, “==”, etc?

For instance, I use to do something like this in fortran:


type test
   a :: Int64
   b :: Float64
end

x = test(1,2.e0)
y = test(2,3e0)

if  x < y 
   do something

but where the “<” is something I defined, such as

x.a < y.a && x.b < y.b

(of course it is easy to define a function isless(…), but for clarity reasons sometimes
it is nice to define the meaning of the operators).

Thank you.

julia> struct test
           a :: Int64
           b :: Float64
           c :: String
           test() = new()
           test(a,b,c) = new(a,b,c)
       end

julia> test()
test(4586250192, 1.5e-323, #undef) # garbage data in isbits fields

julia> test()
test(4493813616, 0.0, #undef)  # garbage data in isbits fields

julia> test(1,2,"foo")
test(1, 2.0, "foo")

julia> Base.:<(x::test, y::test) = x.a < y.a && x.b < y.b

julia> test(1,2,"a") < test(3,2,"b")
false
5 Likes

Just adding information for my own ignorance. Of course, when I thought to define the variable without defining its values, I was willing to define the values afterwards using

x = test()
x.a = 1

in which case, I find now, I need to define a “mutable struct” instead:

mutable struct test
  a :: Int64
  test() = new()
# test() = new(4)  # could be used to start with a default value
  test(a) = new(a)
end

Thank you.

An elegant way (I think) of defining methods for operators is

julia> import Base.<

julia> struct test
                  a :: Int64
                  b :: Float64
                  c :: String
                  test() = new()
                  test(a,b,c) = new(a,b,c)
              end

julia> x::test < y::test = x.a < y.a && x.b < y.b
< (generic function with 71 methods)

julia> test(1,2,"a") < test(3,2,"b")
false

Note that the operator must be imported (or qualified like in Kristoffers example) to be extended.

2 Likes

you can also take a look at the Paramters package (https://github.com/mauro3/Parameters.jl) which allows to define default values for immutable structs via the @with_kw macro.

1 Like

Adding some more information. I am not a huge fan of saving keystrokes :-). The redefinition of the “<” operator can be done using the following, which allows for any complexity of the definition of the operator:

function Base.:<( x::test, y::test )
  if  x.a < y.a
    return true
  else
    return false
  end
end

Additionally, I found out that any function can be used in this way (this is pretty cool):

function in( x::Int64, y::test )
  if x == y.a || x == y.b 
    return true
  end
  return false
end
struct test
  a :: Int64
  b :: Int64
end
x = test(1,3)

julia> 2 in x 
false
julia> 3 in x
true

EDIT: In consideration of the comment below, the “in” function can be written in concise notation using:

in(x::Int64, y::test) = x == y.a || x == y.b

I don’t get it. How is this better than

Base.:<( x::test, y::test ) = x.a < y.a

I think all the redundant code obscures what is going on.

Same thing for the in function.

I didn’t say it is better, it is just another way to write the same thing.

For this specific example of course the concise notation is very nice. But I was thinking on the possibility where the result of the “x < y” involves a complex code, in which case knowing that the same thing can be written as any standard function is good, I think.

For example, it could be that evaluating “x < y” involved solving an optimization problem.

I didn’t really understand the point, I guess. But you mean to point out that you can write it in the ‘standard function format’, like this:

function Base.:<( x::test, y::test )
    ...
end

?

It’s just that I’ve seen before quite a few people write things similar to what you did, such as

if x == true
    return true
elseif x == false
    return false
end

instead of return x, and it just seems so wrong, I have to say something.

2 Likes

I much prefer prefixing with the module when extending methods. It makes it clear from local context what method you are extending (no need to dig through the source code after import statements).

Also, using infix definition is also scary in my opinion, especially in combination with importing,

julia> import Base.<

julia> a < b = 3 # typo, should have been ==
Error showing value of type typeof(<):
SYSTEM: show(lasterr) caused an error
TypeError(:parseint_preamble, "if", Bool, 3)

and julia crashes

3 Likes

@DNF Ah, ok. No, I was just trying to point out the structure, and perhaps the example was too simplistic. I am sorry for posting this long code, I don’t know if in general people think that is helpful.

What I meant is something, lets say, like:

struct quadratic # structure of the data set
  # ax^2 + bx + c
  a::Float64
  b::Float64
  c::Float64
end

function F( f :: quadratic, x :: Float64 ) # F to evaluate the function at x
  return f.a*x^2 + f.b*x + f.c
end 

# the  f1 < f2 operation will be true if there is x0 such 
# that f1(x0) < f2(x) for all x.
function Base.:<( f1::quadratic , f2::quadratic )
  if f1.a < 0. && f2.a > 0. # f1 is concave downward, but not f2
    return true
  end
  xmin1 = -f1.b/(2*f1.a) # x that minimizes f1
  xmin2 = -f2.b/(2*f2.a) # x that minimizes f2
  if F(f1,xmin1) < F(f2,xmin2)
    return true
  end
  return false
end

f1 = quadratic(1.,0.,0.)
f2 = quadratic(1.,0.,-1.)
println( f1 < f2 )
println( f2 < f1 )

Btw, I was using this pattern a lot when coming to Julia - and completely stopped using it over the time.
I try to avoid initializing empty structs and setting up the fields afterwards as much as I can by now.
It just doesn’t look as nice, fragments the initialization code of your struct making it harder to read, makes it impossible to use immutables - and worst of all: you may end up with garbage.

So I usually just define a couple of convenience constructors with reasonable default values - and usually I can stop right there since my use cases are covered.
If that’s not the case for you, the already mentioned https://github.com/mauro3/Parameters.jl ) seems like a good solution.

1 Like

By that you mean something like this?

mutable struct test
  a :: Int64
  b :: Float64
  test() = new(1,1.e0) # this line
  test(a,b) = new(a,b)
end

Yeah something like that… Just usually then as an immutable!
And quite often I will use keyword arguments for the fields, that I don’t often change!

Uhm… not sure what that means. I thought you meant something like

mutable struct test(;a=0,b=1.e0)
   a::Int64
   b::Float64
end
x = test(a=1)

as can be done in function, but that seems not to work for structs.

You put the keywords on the constructor, which is a function (or use https://github.com/mauro3/Parameters.jl).

1 Like

Got it! Make sense! Thank you! (I keep putting the examples, for ignorants like me).

mutable struct test

  a :: Int64
  b :: Float64
  test(a,b) = new(a,b) # without keywords
  test(;a=1,b=2.e0) = new(a,b) # with keywords (both work!)

end

x = test(a=4,b=5.e0)
println(x)

y = test()
println(y)

z = test(3,5.e0)
println(z)

I would write this as

mutable struct Test
    a::Int64
    b::Float64
end

Test(; a=1, b=2.0) = Test(a, b) # with keywords (both work!)

Capital T on struct to comply with style guidelines, no need for .e0, and avoiding inner constructors (unless there is a good reason)

2 Likes

That last example is very clean, but it led me to a think about the possible problems in being able to define a function with the same name of the struct. For example, in this case below, the result is that the first call to test results in defining x as of type test, but the second results in a function value.

Isn’t this problematic, as I could define a new function with the name of a previously defined struct from some module I am using, without knowing it?

(curiously, Julia returns an error if I try to define the struct after the function).

struct test
  a :: Int64
end

function test(;a=3)
  return a
end

x = test(2)
println(x)
> test(2)

x = test()
println(x)
> 3

I don’t think you can, unless you import it or qualify it with a module name.