Converting String to Symbol during struct construction

I am looking at the following

Base.@kwdef mutable struct MyStruct
    id::Int
    value::Symbol = :fixed
end
MyStruct(value::String, kwargs...) = MyStruct(value=Symbol(value), kwargs...)

MyStruct(id=1, value=:a)       # this works
MyStruct(id=2, value="b")      # this does not work

which I naively assumed to be working. But this fails with MethodError: Cannot convert an object of type String to an object of type Symbol. What would be the proper way to implement a “converting constructor” like that?


Edit: I am not using a constructor inside the struct, since using new(...) does not work with keyword arguments, as far as I did understand. Is that correct, or should I keep the constructor inside the struct?

Adding a second constructor

MyStruct(id::Int, value::String, kwargs...) = MyStruct(id=id, value=Symbol(value), kwargs...)

makes this work for me.

You could put the conversion into the constructor:

Base.@kwdef mutable struct MyStruct
    id::Int
    value::Symbol = :fixed
end
MyStruct(value::Symbol, kwargs...) = MyStruct(value, kwargs...)

MyStruct(id=2, value=Symbol(:a))
MyStruct(id=2, value=Symbol("b"))

This seems to only work due to essentially eliminating kwargs... in the MWE. Consider the following change, that breaks it again:

Base.@kwdef mutable struct MyStruct
    id::Int
    value::Symbol = :fixed
    notimportant::Int = -1
end
MyStruct(id::Int, value::String, kwargs...) = MyStruct(id=id, value=Symbol(value), kwargs...)

MyStruct(id=1, value="a")     # this fails again

While this certainly works, it achieves something else and does not solve the initial problem. It converts the String to a Symbol ahead of construction of the struct, instead of during. While this seems like a valid fix, consider the following:

  • I have a large “data storage” (the struct), that is initialized using data parsed from a file (yaml, csv, …). I read in Strings. This is fairly efficient for most fields. Later on I come to a point where profiling shows that keeping a specific field as Symbol makes sense (due the timeloss of doing Symbol("somestring") after reading the data, using Symbols can be worse than Strings for many fields).
  • I construct this struct at several points of my code. If I need to “symbolify” a new field, I need to change this everywhere instead of at a single central point (the constructor).

In this method, value is a positional argument, not a keyword argument. And your kwargs... are actually positional arguments as well. In Julia, unlike Python, keyword arguments are totally distinct from positional arguments.

In consequence, MyStruct(id=2, value="b") does not call this method, and instead calls the lower-level inner constructor of MyStruct, which takes its arguments and calls convert, the same as when you do assignment with =. There is no convert method from String to Symbol (Julia only defines convert when you want conversion to happen implicitly), hence the error.

1 Like

Maybe this helps then

Base.@kwdef mutable struct MyStruct
    id::Int
    value::Symbol = :fixed
    notimportant::Int = -1
    function MyStruct(id, value, notimportant)
        new(id, Symbol(value), notimportant)
    end
end

MyStruct(id=1, value=:a)       
MyStruct(id=2, value="b")      
3 Likes

I’ve previously tried this:

Base.@kwdef mutable struct MyStruct
    id::Int
    value::Symbol = :fixed
end
MyStruct(; value::String, kwargs...) = MyStruct(; value=Symbol(value), kwargs...)

MyStruct(; id=1, value="a")    # fails

Which fails with TypeError: in keyword argument value, expected String, got a value of type Symbol. Your comment suggests that this would be the right direction, but something with my approach is wrong.

Julia doesn’t dispatch on keyword arguments, so this won’t work:

Keyword arguments behave quite differently from ordinary positional arguments. In particular, they do not participate in method dispatch. Methods are dispatched based only on positional arguments, with keyword arguments processed after the matching method is identified.

See the solution by @goerch above. Or you could just write a totally new function, e.g. mystruct(...) = MyStruct(...).

Thanks for that, but I am really looking for a way to utilize the kwargs iterator - this is after all a feature of the language and manually adding tens (or hundreds) of variables to the constructor is not how I feel it “should” work.


This was the important information I somehow missed. Without introducing a function with a different name, this allows the conversion by utilizing an “optional” positional argument like:

Base.@kwdef mutable struct MyStruct
    id::Int
    value::Symbol = :fixed
end
MyStruct(::Type{String}; value, kwargs...) = MyStruct(; value=Symbol(value), kwargs...)

MyStruct(String; id=1, value="a")
MyStruct(; id=1, value=:a)
1 Like

You can also have the conversion happen automatically, without specifying extra type arguments. But I think this requires getting rid of the automatic Base.@kwdef kwargs constructor. For example:

julia> struct MyStruct
           id::Int
           value::Symbol
       end

julia> MyStruct(; value, kws...) = MyStruct((value=Symbol(value), kws...)[fieldnames(MyStruct)])

But then I lose out on using default values for the structs fields. Or is there an alternative way (besides packing them into the constructor)?

You could define your own symbol-wrapper type with a convert method:

struct StringSymbol
    sym::Symbol
end
StringSymbol(s::AbstractString) = StringSymbol(Symbol(s))
Base.Symbol(s::StringSymbol) = s.sym
Base.convert(::Type{StringSymbol}, s::Union{Symbol,AbstractString}) = StringSymbol(s)

Then you can do:

Base.@kwdef mutable struct MyStruct
    id::Int
    value::StringSymbol = :fixed
end

and get

julia> MyStruct(id=1, value=:a)
MyStruct(1, StringSymbol(:a))

julia> MyStruct(id=2, value="b")
MyStruct(2, StringSymbol(:b))

(The compiled code will be equivalent to code with a Symbol field — everything is inlined. You will probably need to define a few more methods on StringSymbol depending on how you use it, to let it be a drop-in replacement for Symbol.)

3 Likes

It doesn’t really seem possible to make a user-defined type behave like a Symbol. For example:

julia> s = :abc
julia> ss = StringSymbol(:abc)

julia> (;[s => 123]...)
(abc = 123,)
# but
julia> (;[ss => 123]...)
ERROR: TypeError: in typeassert, expected Symbol, got a value of type StringSymbol

AFAIK, there are pretty few contexts in Julia that really require a Symbol; basically anything that smells like metaprogramming, e.g. programmatic generation of keyword arguments as in your example.

In @sstroemer’s case, they don’t seem to be doing this as it sounds like they are just using symbols as interned replacements for strings.

Alternatively, it sounds like they may just want something like GitHub - JuliaString/InternedStrings.jl: Fully transparent string interning functionality, with proper garbage collection … that package has suffered from a lack of attention, but it might be worth a little loving care as an alternative for cases where people want to use symbols as fast interned strings?

I’m not sure if that comes at a huge disadvantage somewhere else, but taking your idea and just applying it to the problem of the missing “automatic conversion” from String to Symbol leads to:

Base.@kwdef mutable struct MyStruct
    value::Symbol = :a
end

Base.convert(::Type{Symbol}, s::String) = Symbol(s)

MyStruct()         # works
MyStruct(:b)       # works
MyStruct("c")      # works

Am I missing something, or is that an easy solution to the problem, without any major downsides?!


This is right, I am currently using Symbols in the struct to allow for a faster check for equality (after a few mystruct.value == "something" checks, the cost of initially “symbolifying” the String is outweighed by the gain in speed on the comparison).
But that would mean, if the above approach is okay, I could just change the type of any field, of any struct, at any time from String to Symbol, without doing anything else and interning would automatically happen. That sounds like an extremely convenient/elegant solution!?

A major downside of using Symbol as an interned string is that it will never be garbage-collected. That’s an advantage of the InternedString.jl approach (which uses a WeakKeyDict to store a garbage-collectable cache).

1 Like