Suppose I have an abstract type, and some concrete types, that are easily converted to strings.
For simplicity, let’s make them tag types.
abstract type AbstractTag end
struct TagA <: AbstractTag end
struct TagB <: AbstractTag end
Base.string(::TagA) = "a"
Base.string(::TagB) = "b"
What I want to do now, is go the other way round, and add a function that converts a string into one of my tag types. The naive implementation could look something like:
function to_tag1(str::AbstractString)
str == "a" && return TagA()
str == "b" && return TagB()
error("foo")
end
This works fine, but it has two downsides: The String <=> AbstractTag mapping is encoded twice and the handling of illegal strings (error(“foo”)) needs to be done manually.
So I was thinking, value dispatching could solve these problems and I came up with this solution:
for T in InteractiveUtils.subtypes(AbstractTag)
@eval begin
to_tag2(::Val{Symbol(string($T()))}) = $T()
end
end
to_tag2(str::AbstractString) = to_tag2(Val(Symbol(str)))
Now it is obviously harder to understand what happens here but it is a lot less code if there are many tags and illegal strings “naturally” throw errors.
But the string to symbol conversion - needed so I can actually use value dispatching - and the subtype iterating smell kind of hacky to me.
My question is, am I overlooking some obvious solution that is both free of redundant information and easy to understand?
If each type is a singleton (ie one unique value, like in your example), you could use a Dict
for lookup. It is of course not type stable, but parsing generally isn’t.
Using the type system for these calculations is generally neither recommended nor necessary; it does not buy you anything.
1 Like
The operation Symbol(str)
needs to look up str
in a giant hash-map. This is expensive. Next, Val{sym}
needs to walk some giant tree in order to find the corresponding type. This is expensive. Furthermore, Symbol
is subtly different from julia string, e.g. julia strings may contain null bytes, symbols cannot.
I recommend to keep your first variant or make a Dict.
1 Like
Yeah, that solution 2 is misusing the type syste was kind of my thinking as well…
Using one dict for each direction seems kind of ok to me, although it does not feel very clean either…
const tagDict = Dict(
TagA() => "a",
TagB() => "b")
const revTagDict = Dict(value => key for (key, value) in tagDict)
Base.string(t::AbstractTag) = tagDict[t]
to_tag3(str::AbstractString) = revTagDict[str]
I was thinking of something along the lines of
function to_tag end
let TAGS = Dict{String,Any}()
for (T, str) in [(:TagA, "a"),
(:TagB, "b")]
@eval begin
struct $T end
Base.string(::$T) = $str
$TAGS[$str] = $T()
end
end
global to_tag(str) = TAGS[str]
end
which hides the dictionary from the user and generates it automatically. You can add bells & whistles like a meaningful error message instead of a KeyError
, etc.
In itself, that isn’t very readable either, is it?
But it should work nicely with the code I already have as my types are already generated after all.
Thanks for your input, both of you
Personally I find it very readable, but I am biased since I wrote it A looped @eval
is a pretty standard idiom in Julia for DRY, and so is a let
-over-something for encapsulation.