Replicating Ruby's dig() in Julia

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?

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
1 Like
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)
6 Likes
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
3 Likes

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…

1 Like

This is just what I was looking for. One nice thing about Ruby’s dig is it can be used to access nested arrays as well as dicts. This is handy for working with responses from Elasticsearch. So I would expand @Amval’s answer to:

dig(x) = x
dig(x::Nothing, keys...) = nothing
dig(d::AbstractDict, key, keys...) = dig(get(d, key, nothing), keys...)
dig(a::AbstractArray, i::Integer, keys...) = checkbounds(Bool, a, i) ? dig(a[i], keys...) : nothing

Then it can do:

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

julia> dig(nested, :a, 2, :c)
"c"
1 Like

You may be interested in Setfield.jl which implements a “lens” API that looks similar to what you’re doing.

2 Likes