Default value of *some* fields in a mutable struct

Hi guys,

just a specific question here: I would like to create a data type with several fields, and give a default value to some of them, but not all. But more specifically, I would like to have only this value appear in the new() constructor, something like

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

which doesn’t work by itself. I don’t want to give explicitly a value to all the
existing fields as I’m still developing my code and I’m constantly putting things in and out. So the question is whether it is possible to assign default values to just some of the listed fields…

Best regards and thanks,

Ferran.

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

If you want to leave default constructor:

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

function coly8(y::Float64, z::Bool, t::Int32, u::Float32, v::Bool)
    return coly8(1, y, z, t, u, v)
end

I guess you were looking for something simpler, like:

mutable struct coly8
           x::Int32 = 1
           y::Float64
           z::Bool
           t::Int32
           u::Float32
           v::Bool
       end
ERROR: syntax: "x::Int32 = 1" inside type definition is reserved
Stacktrace:
 [1] top-level scope at REPL[6]:1

It not gonna work, but I can see the syntax is reserved, so probably will be possible in future

2 Likes

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
9 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:

1 Like

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.

9 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)
5 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)
2 Likes

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.

1 Like

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
3 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.

8 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