How to use @?
using Maybe
using Maybe: @somethingIntroduction
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) === nothingIt 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) === nothingTo 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) === nothingSimilarly, @? 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]) === nothingThis 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) === nothingObserve 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) + 1Some(2)
@? is evaluated to nothing when the first sub-expression is evaluated to nothing:
r = @? maybe_positive(-1) + 1
@assert r === nothingLiteral nothing is transformed to Some(nothing):
@? nothingSome(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 === nothingNote that demo(2, ...) is not called:
ARG_HISTORY1-element Vector{Any}:
1 => nothingThis 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_HISTORY2-element Vector{Any}:
1 => nothing
2 => nothingThis 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 === nothingFinally, 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]) === nothingIndex access can be "fused" with other function calls:
@assert (@? dict[:a][:c] + 1) === Some(3)
@assert (@? dict[:a][:d] + 1) === nothingExisting 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) === nothingSince 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 === 1Indexing 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)) == 1Alternatively, 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)) === 1Note 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]) === nothingCombining @? 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)) === nothingFunctions
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)) == nothingThus, 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
endSome(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.