Default value of *some* fields in a mutable struct

Hi,
thanks for the input, but as I wrote on the message, that kind of constructors are exactly what I want to avoid :slight_smile: I do not want to have to specify default values for all the fields, nor list them in the constructor: just want to specify the default value of one field, and not even list the others. As I said, this is because I’m not sure what fields will end up being there, as I’m still developing my code and changing many things in many different structs like the model one I wrote. I was hoping for something similar to what I wrote, where in the new() statement only the K.x=1 appears, with no (explicit or implicit) reference to any other field. In that sense, what you wrote at the end could have the same effect -though it is not allowed :frowning:

Any other hints? Or shall I assume that this is a (current?) limitation of the language?

Best,

Ferran.

This seems like a great use case for a macro. You might take inspiration from https://github.com/mauro3/Parameters.jl which does something similar:

julia> using Parameters

julia> @with_kw struct Foo
         x::Int32 = 1
         y::Float64
       end
Foo

julia> Foo(y = 2)
Foo
  x: Int32 1                                                                                                                                                                                                                                                                                                                          
  y: Float64 2.0 

Note that @with_kw doesn’t give the behavior you’re looking for. Instead, it throws an error if any field without a default is not provided:

julia> Foo()
ERROR: Field 'y' has no default, supply it with keyword.

But that’s an intentional design decision, and you could write something similar to @with_kw that would do exactly what you want. There is also related work in @kwdef which is supplied with Julia base:

julia> Base.@kwdef struct Bar
         x::Int32 = 1
         y::Float64
       end
Bar

julia> Bar(y = 2)
Bar(1, 2.0)

julia> Bar()
ERROR: UndefKeywordError: keyword argument y not assigned
3 Likes

“Limitation” is not in Julia’s vocabulary. I was going to suggest a macro too.

1 Like

Very interesting this discussion about the macro, but that in the end does not solve the problem as you mention. Maybe I would need to write a complex macro for that, but sounds somewhat weird compared to other languages where you can give default values in the structs directly without too much hassle… I was hoping for something simple :slight_smile:

A related question is: are you sure you want to leave the fields literally undefined? There’s no programmatic way to tell the difference between an ::Int32 field that you left uninitialized and one you filled with a value. Even an uninitialized Int32 will have a value, it’s just an undefined value which could be any number and could change each time you run your program.

What if, instead, you used a Union{Int32, Missing} to indicate that there might be a value. Then the default is trivial (it’s missing):

julia> Base.@kwdef struct Foo
         x::Union{Int32, Missing} = missing
         y::Union{Float64, Missing} = 1.0
       end
Foo

julia> Foo()
Foo(missing, 1.0)

julia> Foo(5, 1.0)
Foo(5, 1.0)

julia> Foo(y = 2.5)
Foo(missing, 2.5)

julia> f = Foo(y = 2.5)
Foo(missing, 2.5)

julia> f.x
missing

This gives you safer code, with obvious default behavior, at very little run-time performance cost.

3 Likes

I was looking for something like this earlier, and went with Union{Int64, Missing}. You can initialize your structure variables with missing values which lets you know precisely which values are missing and take care of them gracefully.

Alternatively, you could simply assign a rand() to each variable, but this seems very error prone and not good practice.

Agree, probably it will be improved in future. Union{Int64, Missing} from answers above should also be shortened to Int64? or similar.

Ok thank you again… I see smart solutions to the problem, butno matter what, you still have to give default values to all variables, at least in the definition of each field with the @kwdef macro as proposed above. Much better than having to state a value for each specific instance and solves the problem in fact.
Still, I’m amazed by the fact that there is no way to specify the default value of one single field and state nothing about the rest… keep thinking that this is a language limitation (a word you would definitively find in Julia’s dictionary, as in all other languages :slight_smile:
Thanks for your help,
Ferran

Hola Ferran :slight_smile:

Maybe using a constructor with optional arguments solves your use case?

julia> mutable struct Coly1
           x :: Int32
           y :: Float64
           z :: Bool
           t :: Int32
           u :: Float32
           v :: Bool
       end;

julia> function Coly1(;x=0,y=0,z=false,t=0,u=0,v=false)
           return Coly1(x,y,z,t,u,v)
       end

# We can create a Coly1 instance specifying only one of the arguments.
julia> p = Coly1(y=3.14)
Coly1(0, 3.14, false, 0, 0.0f0, false)

# Or a subset of the arguments
julia> p2 = Coly1(y=3.14,z=true)
Coly1(0, 3.14, true, 0, 0.0f0, false)

# Or none of the arguments
julia> p2 = Coly1()
Coly1(0, 0.0, false, 0, 0.0f0, false)

it is true that the consturctor Coly1(;x=0,y=0,z=false,t=0,u=0,v=false) has some predefined values but this happens as well in other languages like Python:

>>> class ColyPy(object):
...     def __init__(self, x, y=0):
...         self.x = x
...         self.y = y

# This does not break because y has a predefined value
>>> p = ColyPy(x=2)

# This breaks because x has no value
>>> ColyPy(y=2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() missing 1 required positional argument: 'x'

If you want to force a user to specify some of the arguments in a struct (like the pyhon case above)

mutable struct Coly4
    x :: Int32
    y :: Float64
    z :: Bool
    t :: Int32
    u :: Float32
    v :: Bool
end;

function Coly4(x;y=0,z=false,t=0,u=0,v=false)
    return Coly4(x,y,z,t,u,v)
end

# This will not work because x is necessary
julia> Coly4()
MethodError: no method matching Coly4()

# You can still specify only x, (the first argument), to construct a Coly4
julia> Coly4(2)
Coly4(2, 0.0, false, 0, 0.0f0, false)
4 Likes

I coded many languages and some Julia behaviours are bothering me too, but not this one. Could you tell which language’s behaviour is perfect for you?
I recalled when I wrote getters and setter methods in Java there were code generator integrated in IDE for that. Maybe we can go this way and script constructor generating in vscode or atom. I won’t be surprised if someone did it already.

And finally for me code below is very clear and I cannot imagine how to solve it better with a macro. Also solution from @davidbp is nice, there are so many options.

using Parameters
@with_kw mutable struct coly8
   x::Int32 = 1
   y::Float64
   z::Bool
   t::Int32
   u::Float32
   v::Bool
end

data = coly8(y=1.2, z = true, t = 5, u=4.0, v=true)

Hi guys (hola Davis :slightly_smiling_face:

first thing and to answer the first question: no programming language I have found so far seems perfect for me, all of them have enbeded decisions that you could discuss at least. Stating that “Limitation is not in Julia’s vocabulary” implies that everything can be done in Julia, which I believe is difficult to back up, as in any other programming language…

Now regarding the new solutions… again, they are not solutions as long as you have to give default values to all the fields, either in the constructor and when constructing an instance of that type. Please notice that I can leave undefined fields if I do not care about default values, like in

mutable struct croco
  x :: Float64
  y :: Bool
  croco() = new()
end

krok = croco()
> croco(5.0e-324, false)

now you may argue this is bad programming practice since you do not have control over the underlying values at construction time, etc etc, but that is not the question here: the fact is that I could create krok without saying anything about its fields. I was wondering if there was a way to ONLY set y to true as default (for instance) in a similar way, but apparently I can’t find it…

Best regards and thanks again,

Ferran.

You can set even set the constructor inside the struct so that you have this behaviour

julia> mutable struct croco2
         x :: Float64
         y :: Bool
         croco2(;x=1,y=false) = new(x,y)
       end

julia> croco2(x=2)
croco2(2.0, false)

julia> croco2()
croco2(1.0, false)

I know, I am still setting each field with a particular value to the constructor because Julia does not create this type of constructors automatically. Nevertheless, you only need to know a particular instance of every type you put in the fields in the constructor.

Is this that rare? Well python class constructors have the same default behaviour. You can have keyword arguments like in ColyPy I wrote above but you need to specify a default value to the arguments. I am not sure if there is any other language where structs can do what is proposed here by default, but It would be cool to be able to do

mutable struct croco_randomdefault
  x :: Float64
  y :: Bool
  croco4(;x::Float64, y::Bool) = new()
end

So that both

krok = croco_randomdefault(x=1)
> croco(1, julia_stores_here_true_or_false)

krok = croco_randomdefault(y=true)
> croco(julia_stores_here_any_float, true)

work. (EDIT: Answered below by Gunnar)

I might be misunderstanding something but isn’t the whole problem that you failed to return the object from the constructor? This seems to do what you want.

mutable struct coly8
    x :: Int32
    y :: Float64
    z :: Bool
    t :: Int32
    u :: Float32
    v :: Bool
    coly8() = ( K = new(); K.x = 1; return K )
end
2 Likes

I was not aware that this is possible :open_mouth:

For example from the original post this also works as @Ferran_Mazzanti requested

mutable struct coly8
    x :: Int32
    y :: Float64
    z :: Bool
    t :: Int32
    u :: Float32
    v :: Bool
    coly8() = new(1)
end

This allows you to create coly8() with a specific value for x hardcorded in the constructor. What about defining a subset of values when the instance is created?

mutable struct coly_xy
           x :: Int32
           y :: Float64
           z :: Bool
           t :: Int32
           u :: Float32
           v :: Bool
           coly_xy(x,y) = new(x,y)
       end

This seems to be a nice example of how to create a struct where you expect users to specify some of the fields (here x,y) but the others are instanciated “randomly”.

julia> coly_xy(1,2)
coly_xy(1, 2.0, false, 1, 0.0f0, false)

julia> coly_xy(1,2)
coly_xy(1, 2.0, false, 1, 5.899801f-21, true)

Now I am only missing how to do this for any subset of the fields without specifying the values of the complementary part of the fields. Any ideas?.

Are you looking for something like this?

mutable struct coly_xy
    x :: Int32
    y :: Float64
    z :: Bool
    t :: Int32
    u :: Float32
    v :: Bool
    function coly_xy(;kwargs...)
        K = new()
        for (key, value) in kwargs
            setfield!(K, key, value)
        end
        return K
    end
end

It allows you to specify the fields you want to set by name.

julia> coly_xy(t=Int32(13), v=true)
coly_xy(1, 1.0e-323, false, 13, 3.9061288f-19, true)

For more convenience you may want to add a convert call if the type doesn’t match.

4 Likes

Awesome! It can be wrapped into a macro for nicer syntax. Great solution.

That is awesome! Thank you Gunnar.

This is why I love Julia, hackable all the way down to the bit.

mutable struct coly_rand
    x :: Int32
    y :: Float64
    z :: Bool
    t :: Int32
    u :: Float32
    v :: Bool
    
    function coly_rand(;kwargs...)
        K = new()
        for (key, value) in kwargs
            field_type_key = typeof(getfield(K,key))
            setfield!(K, key, convert(field_type_key, value))
        end
        return K
    end
    
end

This pretty much is completly generic

coly_rand(x=2,y=2)
coly_rand(2, 2.0, true, 0, 2.55f-43, false)
coly_rand(x=2)
coly_rand(2, 1.1609548413287613e-28, false, 1702389026, 0.0f0, false)
2 Likes

What behavior do you want the non-initialized fields to have? Presumably to throw an error for any operation using them? Or do you want the non-initialized values to propagate and return other non-initialized values after operations?

This is the difference between nothing and missing in Julia. Since Julia aims to have both more “programming” and more “data science” uses, we don’t want to require a single default behavior for non-initialized values.

The Union{Nothing, Int64} or Union{Missing, Int64} is the correct solution to your problem, and it has the benefit of being explicit about the behavior of your uninitialized values.

@davidbp, great improvement!

I wrote a stub of the macro I mentioned:

macro awesome(s)
    if s.head !== :struct 
        error("Not a struct def")
    end 

    ismutable = s.args[1]
    if !ismutable
        error("Not mutable")
    end
    
    name = s.args[2]
    body = s.args[3]    

    ctor = :(function $(name)(;kwargs...)
                K = new()
                for (key, value) in kwargs
                    field_type_key = typeof(getfield(K,key))
                    setfield!(K, key, convert(field_type_key, value))
                end
                return K
            end)
            
    newbody = [body.args; ctor]
    
    return Expr(s.head, ismutable, name, Expr(body.head, newbody...))       
end

Now we can write:

@awesome mutable struct coly8
    x :: Int32
    y :: Float64
    z :: Bool
    t :: Int32
    u :: Float32
    v :: Bool
end
julia> coly8(u=4.0)
coly8(8, 9.200841125e-315, true, 0, 4.0f0, true)

The next step would be parsing a expression in a struct with predefined constants or use the x::Int = 42 syntax like in Parameters.jl. Truly limitless set of options :slight_smile: I would add also zeroing of not defined fields like most of languages do.

5 Likes