Converting string to concrete type

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? :slight_smile:
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 :wink: A looped @eval is a pretty standard idiom in Julia for DRY, and so is a let-over-something for encapsulation.