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?
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 Symbolahead 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.
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.
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 @goerchabove. 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:
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:
(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.)
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.
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).