Replicating Ruby's dig() in Julia

question

#1

Hello,

in Ruby there’s a method useful for navigating nested dictionaries called dig()

nested = Dict(
    :a => Dict(
        :b => "b",
        :c => Dict(
            :d => "d"
        )
    )
)

dig(nested, :a, :b)  #=> "b"
dig(nested, :a, :c, :d)  #=> "d"
dig(nested, :a, :e)  #=> nothing

I have written this function. The problem is that args... will produce a Tuple. When called recursively, i will have a Tuple of Tuples and I don’t know how to flatten it or to avoid this. I can fix it by making args::Tuple and calling like this: dig(nested, (:a, :b)), but I find this somewhat uglier.

function dig(dict::Dict, args...)
    if haskey(dict, args[1])
        aux = dict[args[1]]
        if length(args[2:end]) > 0
            dig(access, args[2:end])
        else
            aux
        end
    end
 end

How can I “splat” args when maing the recursive call or flatten the Tuple?


#2
function dig(dict::Dict, args...)
    d = dict
    for key in args
        if haskey(d, key)
            d = d[key]
        else
            return nothing
        end
    end
    d
end

#3
dig(dict::AbstractDict, key, keys...) = dig(dict[key], keys...)
dig(x) = x

This throws KeyError rather than returning nothing on missing key, but should be easy to modify if you really want to return nothing as in your example.
EDIT: alternately

dig2(dict::AbstractDict, keys...) = foldl(getindex, keys; init=dict)

#4
dig(x) = x
dig(d::AbstractDict, key, keys...) = dig(get(d, key, nothing), keys...)

get(dict, key, default) is really nice for dictionaries, and also faster than first checking (haskey) and then retrieving.

Edit: The foldl solution is much faster, but fails when the key isn’t found:

julia> dig2(d, :a, :e)

ERROR: KeyError: key :e not found

#5

Thank you all four your answers.

@DNF I didn’t find foldl to be much faster, at least after doing the necessary modifications so it doesn’t fail when the key isn’t found. And @yha first approach I’d say is conceptually simpler. Your solution is closer, although it would fail when passed :a, :b, :c if :bdoesn’t exist already.

Can be fixed like this:

dig(x) = x
dig(x::Nothing, keys...) = nothing
dig(d::AbstractDict, key, keys...) = dig(get(d, key, nothing), keys...)

But maybe I shouldn’t be trying to navigate through empty keys on the first place…