Equivalent of Clojure `some->` for nested data?

I’m a happy user of both Chain.jl and Accessors.jl, but I find myself missing a few things from Clojure. I’m not sure if they exist, and I just don’t know about them, or if these are actual missing capabilities.

I think one motivating example is how easy it is in Clojure to extract a value from a nested data structure and pass it to a function. For example, you might have:

(some-> data
        :foo
        :bar
        baz)

This is a bit like unpacking a Dict in Julia and passing it to a function, like data[:foo][:bar] |> baz, but there are some extra niceties that arise from how extensively and consistently Clojure uses nil. First, the :foo keyword works like a function in Clojure and is similar to x -> get(x, :foo, nothing) in Julia. Second, some-> will short circuit and return nil as soon an intermediate value is nil. So, if we had a @chain_some macro, if might expand to something like:

if isnothing(data)
    nothing
else
    foo_ = data[:foo]
    if isnothing(foo_)
        nothing
    else
        bar_ = foo_[:bar]
        if isnothing(bar_)
            nothing
        else
            baz(bar_)
        end
    end
end

So, combining those, I guess I wish I could write something like:

@chain_some data begin
    get(_, :foo, nothing)
    get(_, :bar, nothing)
    baz
end

This has all been focused on Dict, but in reality I have a mix of Dicts, NamedTuples, and structs. My get calls above would work for Dicts and NamedTuples, but not for structs. This seems like something that Accessors.jl could provide a generalized solution to, and I see @maybe in AccessorsExtra.jl, but that doesn’t seem to be quite what I want. I think I’m looking for something like @maybe that could do @maybe_nested data.foo[:bar], and if any intermediate value is nothing, it short circuits and evaluates to nothing.

So, putting it all together again, perhaps something like:

@chain_some data begin
    @maybe_nested _.foo[:bar]
    baz
end

If you’ve made it this far, thanks for sticking with me. If you have suggestions for where this might already exist in the package ecosystem, or perhaps an alternative way of doing this, I’d love to hear about it! Thanks!

1 Like

I’m not aware of packages providing a nice chaining syntax for this, but the core functionality can be implemented by defining

julia> getsomething(x, i) = isnothing(x) ? x : get(x, i, nothing); # or use dispatch with a x::Nothing case

julia> x = (; a = (; a1 = 1, a2 = 2), b = (; b1 = 3))
(a = (a1 = 1, a2 = 2), b = (b1 = 3,))

julia> getsomething(getsomething(x, :a), :a2)
2

julia> getsomething(getsomething(x, :c), :a2) |> isnothing
true

If nothing else is available, you could make a macro that nests these getsomething calls with a nice syntax, if you want. Or you can make a currying function

julia> tryget(i) = x -> isnothing(x) ? x : get(x, i, nothing);

julia> x |> tryget(:a) |> tryget(:a2)
2

julia> x |> tryget(:c) |> tryget(:a2) |> isnothing
true

You can make a function applysomething(f, x) = isnothing(x) ? x : f(x) (or a currying version like applysomething(f) = x -> isnothing(x) ? x : f(x)) for applying a function to non-nothing arguments.

1 Like

A huge nested data structure, mostly vectors and maps. It is actually a tree. How to find a property that is nested deep inside the tree?

That’s exactly what @maybe does :slight_smile: There’s even a convenience macro @oget based on maybe:

julia> data = (a=1, b=[2], c=nothing)
(a = 1, b = [2], c = nothing)

julia> @oget data.a
1

julia> @oget data.xxx

julia> @oget data.b[1]
2

julia> @oget data.b[2]

julia> @oget data.c[123]

julia> @oget sin(data.c[123])
1 Like

Thank you! @oget is exactly what I was looking for! It’s a shame it’s completely undocumented in AccessorsExtra.jl. Perhaps I’ll take a stab at some documentation when I get a chance.

1 Like

Happy that it works for you :slight_smile: A lot of stuff in AccessorsExtra.jl, including maybe, were originally proof of concepts – to see how they fit Julia. Then, documentation has definitely lagged behind actual development…
maybe optics are mentioned both in the readme and in the docs, but I agree it’s very barebones and doesn’t include @oget – although that’s how I use them most often! Documentation is specifically set up to reduce friction, it’s just a readme + a pluto notebook, hope it should be straightforward to contribute.

1 Like