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.