How to use @?

using Maybe
using Maybe: @something

Introduction

Julia Base provides functions findfirst and findlast that returns an integer when an element is found:

findfirst(x -> gcd(x, 42) == 21, 50:200)
14

and nothing if not:

@assert findlast(x -> gcd(x, 42) == 23, 50:200) === nothing

It is rather tedious to combine such functions:

function find_some_random_range_1(data)
    f(x) = gcd(x, 42) == 21
    i = findfirst(f, data)
    i === nothing && return nothing
    j = findlast(f, data)
    j === nothing && return nothing
    return data[i:j]
end

@assert find_some_random_range_1(50:200) === 63:189
@assert find_some_random_range_1(30:50) === nothing

To solve this issue, Maybe.jl provides a macro @? that lets you write

function find_some_random_range_2(data)
    f(x) = gcd(x, 42) == 21
    @? begin
        i = findfirst(f, data)
        j = findlast(f, data)
        return data[i:j]
    end
end

@assert find_some_random_range_2(50:200) === Some(63:189)
@assert find_some_random_range_2(30:50) === nothing

Similarly, @? is also useful for indexing into arrays, dictionaries, etc. that may fail. For example

dict = Dict(:a => 1, :b => nothing, :c => 2)
@assert (@? dict[:a] + dict[:c]) == Some(3)
@assert (@? dict[:a] + dict[:non_existing_key]) === nothing

This is explained in more details in Indexing section below.

How it works

The above example find_some_random_range_2 is roughly equivalent to

function find_some_random_range_3(data)
    f(x) = gcd(x, 42) == 21
    mi = findfirst(f, data)
    mi === nothing && return nothing  # (1)
    i = something(mi)                 # (2)
    mj = findlast(f, data)
    mj === nothing && return nothing  # (1)
    j = something(mj)                 # (2)
    md = Maybe.getindex(data, i:j)    # (3)
    md === nothing && return nothing  # (1)
    d = something(md)                 # (2′)
    return Some(d)                    # (4)
end

@assert find_some_random_range_3(50:200) === Some(63:189)
@assert find_some_random_range_3(30:50) === nothing

Observe that:

(1) If a function returns nothing, the whole evaluation short-circuits and evaluates to nothing. (Side notes: short-circuiting is not actually implemented using return in @? so that it can be used outside functions.)

(2) The returned value is always unwrapped by something. Thus, it works with "ordinary" functions like + as well as a function returning Some (like Maybe.getindex); see (2′).

(3) Indexing dispatches to Maybe.getindex.

(4) Finally, the returned result is always ensured to be wrapped by Some.

More examples

Consider a function that returns nothing on "failure":

maybe_positive(x) = x > 0 ? x : nothing;

When a function call in @? is evaluated to a non-nothing, the returned value is wrapped in Some:

@? maybe_positive(1)
Some(1)

When Some appears in the argument positions, they are automatically un-wrapped:

@? maybe_positive(1) + 1
Some(2)

@? is evaluated to nothing when the first sub-expression is evaluated to nothing:

r = @? maybe_positive(-1) + 1
@assert r === nothing

Literal nothing is transformed to Some(nothing):

@? nothing
Some(nothing)

@? terminates the call chain immediately when it sees nothing as the return value.

ARG_HISTORY = []
demo(label, x) = (push!(ARG_HISTORY, label => x); x)

r = @? demo(2, identity(demo(1, nothing)))
@assert r === nothing

Note that demo(2, ...) is not called:

ARG_HISTORY
1-element Vector{Any}:
 1 => nothing

This can be avoided by prefixing the function name by $:

empty!(ARG_HISTORY)

@? $demo(2, $identity($demo(1, nothing)))
Some(nothing)

Now demo(2, ...) is called:

ARG_HISTORY
2-element Vector{Any}:
 1 => nothing
 2 => nothing

This is because @? automatically unwraps Some in the call chain. It means that Some acts like the identity function inside @?:

@? Some(Some(Some(nothing)))
Some(nothing)

On the other hand, identity acts like "unwrap" function:

r = @? identity(identity(Some(nothing)))
@assert r === nothing

Finally, with $Some:

@? $Some($Some($Some(nothing)))
Some(Some(Some(Some(nothing))))

$(expression) can be used to evaluate the whole expression in the normal context

@? $(Some(Some(Some(nothing))))
Some(Some(Some(Some(nothing))))

Indexing

Index access is also lifted. Consider a dictionary

dict = Dict(:a => Dict(:b => nothing, :c => 2));

Any value stored in the container (here, a Dict) is returned as a Some:

@? dict[:a][:c]
Some(2)

Thus, nothing stored in the container is returned as Some(nothing)

@? dict[:a][:b]
Some(nothing)

On the other hand, accessing non-existing index returns nothing:

@assert (@? dict[:a][:d]) === nothing
@assert (@? dict[:d]) === nothing

Index access can be "fused" with other function calls:

@assert (@? dict[:a][:c] + 1) === Some(3)
@assert (@? dict[:a][:d] + 1) === nothing

Existing nothing value can be normalized using something as usual inside @?. This is because @? automatically unwraps Some once.

@assert (@? something(dict[:a][:b], 0) + 1) === Some(1)
@assert (@? something(dict[:a][:c], 0) + 1) === Some(3)
@assert (@? something(dict[:a][:d], 0) + 1) === nothing

Since identity acts like unwrapping operation inside @?, it can be used for normalizing non-existing key and existing nothing to the same value:

@assert something((@? identity(dict[:a][:b])), 0) + 1 === 1
@assert something((@? identity(dict[:a][:c])), 0) + 1 === 3
@assert something((@? identity(dict[:a][:d])), 0) + 1 === 1

Indexing also works with arrays

vectors = [[1, 2], [3, 4, 5]]

@assert (@? vectors[1][2]) === Some(2)
@assert (@? vectors[3][4]) === nothing
@assert (@? vectors[1][3]) === nothing

@? return

return in @? is a powerful pattern for returning a non-nothing value.

function first_something(dict)
    @? return dict[:a]
    @? return dict[:b]
    @? return dict[:c]
    return nothing
end

@assert first_something(Dict()) == nothing
@assert first_something(Dict(:c => 3)) == Some(3)
@assert first_something(Dict(:a => 1, :c => 3)) == Some(1)

Use $return(x) to avoid the returned value to be automatically wrapped by Some. Note that the parentheses are required.

function first_something2(dict)
    @? $return(dict[:a])
    @? $return(dict[:b])
    @? $return(dict[:c])
    return nothing
end

@assert first_something2(Dict()) == nothing
@assert first_something2(Dict(:c => 3)) == 3
@assert first_something2(Dict(:a => 1, :c => 3)) == 1

Alternatively, combined with @something:

function first_something3(dict)
    return @something {
        @? dict[:a];
        @? dict[:b];
        @? dict[:c];
        0;  # fallback
    }
end

@assert first_something3(Dict()) === 0
@assert first_something3(Dict(:c => 3)) === 3
@assert first_something3(Dict(:a => 1, :c => 3)) === 1

Note that @? can have multiple statements. Any of the sub-expression evaluating to nothing short-circuits to the end of @? block.

function a_plus_b(dict)
    @? begin
        a = dict[:a]
        b = dict[:b]
        return a + b
    end
    return 0
end

@assert a_plus_b(Dict(:a => 1)) == 0
@assert a_plus_b(Dict(:a => 1, :b => 2)) == Some(3)

Note that @? works with other control flows like break:

function something_positive_add3(xs, idx)
    found = nothing
    for i in idx
        @? if xs[i] > 0  # out-of-bound access is ignored
            found = i
            break
        end
    end
    return @? xs[found] + 3
end

@assert something_positive_add3([-1, 0, 1], [0, 1, 3]) === Some(4)
@assert something_positive_add3([-1, 0, 1], [0, 1, 2]) === nothing

Combining @? and @something

As explained, using @? in @something is well supported. However, further nesting is not supported (as the same expression would be processed by @? twice). Assigning to an intermediate variable is a safe way to use the result of @something in @?. The pattern @something(..., return) is useful inside functions for this.

function extract_a_and_bc(x)
    c = @something {
        @? x[:b][:c];
        @? x[:b][:ccc];
        return;  # filter out if none of them exist
    }
    return @? (a = x[:a], c = c)
end

@assert extract_a_and_bc(Dict(:a => 1, :b => Dict(:c => 2))) === Some((a = 1, c = 2))
@assert extract_a_and_bc(Dict(:a => 1)) === nothing
@assert extract_a_and_bc(Dict(:a => 1, :b => Dict())) === nothing
@assert extract_a_and_bc(Dict(:b => Dict(:c => 2))) === nothing
@assert extract_a_and_bc(Dict(:a => 10, :b => Dict(:ccc => 20))) === Some((a = 10, c = 20))

In a rare situation, it may be useful to use $(...) to nest @something-of-@?s in @?:

function a_plus_b_plus_c_or_d_times_2(x)
    @? begin
        p = x[:a] + x[:b]
        q = $(@something {
            @? p + x[:c];
            @? p + x[:d];
            Some(nothing);  # `return` works as well
        })
        return 2q
    end
end

@assert a_plus_b_plus_c_or_d_times_2(Dict(:a => 1, :b => 2, :c => 3)) == Some(12)
@assert a_plus_b_plus_c_or_d_times_2(Dict(:a => 1, :b => 2, :d => 3)) == Some(12)
@assert a_plus_b_plus_c_or_d_times_2(Dict(:a => 1, :b => 2)) === nothing

Functions

When @? sees functions (including closures and do blocks ), it converts them recursively to operate on Union{Some{T},Nothing}.

@? get_a_plus_b(dict) =
    get(dict, :a_plus_b) do
        dict[:a] + dict[:b]
    end

@assert get_a_plus_b(Dict(:a_plus_b => 1)) == Some(1)
@assert get_a_plus_b(Dict(:a => 1, :b => 2)) == Some(3)
@assert get_a_plus_b(Dict(:b => 1)) == nothing

Thus, functions created with @? work nicely in @?:

@? begin
    x = get_a_plus_b(Dict(:a_plus_b => 1))
    y = get_a_plus_b(Dict(:a => 1, :b => 2))
    x + y
end
Some(4)

Debugging

You can include debugging information to @? expr with :debug flag as in @? :debug expr. This inserts @debug logging statements for every possible short-circuit points. When using the standard Julia logger, this information can be printed by setting the environment variable:

ENV["JULIA_DEBUG"] = "all"  # or narrower scope like "MyPackage"

This logging statement prints the expression that is evaluated to nothing and all the local variables using Base.@locals.

dict = Dict(:a => Dict(:b => nothing, :c => 2))
@? :debug dict[:a][:d]
┌ Debug: Got `nothing`. Short-circuiting...
│   evaluating = (Maybe.getindex)(dict[:a], :d)
│   locals = Dict{Symbol, Any}(:dict => Dict{Symbol, Dict{Symbol, Union{Nothing, Int64}}}(:a => Dict(:b => nothing, :c => 2)))
└ @ Main.var"ex-lift-macro" none:4

@? :debug expr also inserts a call to a no-op function Maybe._break. When using Debugger.jl, the state just before exit can be examined by adding it to the breakpoint with bp add Maybe._break.

julia> using Maybe, Debugger

julia> dict = Dict(:a => Dict(:b => nothing, :c => 2));

julia> f(dict) = @? :debug dict[:a][:d];

julia> @enter f(dict)
In f(dict) at REPL[4]:1
>1  f(dict) = @? :debug dict[:a][:d]

About to run: return Maybe.Implementations.nothing
1|debug> bp add Maybe._break
[ Info: added breakpoint for function _break
1] _break

1|debug> c
Hit breakpoint:
failed to lookup source code, showing lowered code:
In _break() at /home/takafumi/.julia/dev/Maybe/src/lift.jl:51
>1  1 ─     return Maybe.Implementations.nothing

About to run: $(Expr(:meta, :noinline))
1|debug> up
In f(dict) at REPL[4]:1
>1  f(dict) = @? :debug dict[:a][:d]

About to run: (Maybe._break)()
2|debug>

This page was generated using Literate.jl.