I could express a certain class computation with variations on composing small but occasionally expensive functions. Sometimes the functions produce Nullables, in which case I want computation to short-circuit and not compute the other expensive parts.
MWE:
f(a) = (println("fcall was expensive"); a.+1)
g(b) = (println("gcall even more expensive"); b.+2)
h(x,y) = x.+y
h(f(1),g(2)) # paid the price
h(f(Nullable{Int}()),g(2)) # this should short circuit, no gcall
h(f(9),g(Nullable{Int}())) # it would be nice if this did, too, ie no fcall
I could express the logic with a bunch of conditionals, but if there is an idiomatic way, it would make my code very compact and organized and I would love that. Suggestions are appreciated.
I think that broadcast makes no assertions about how many times your function gets called (this came up in the discussion of broadcast over sparse arrays where you would want to check if the function preserves sparsity by calling it once). g(2) will always be called though (as I understand it).
f(a) = (println("fcall was expensive"); a+1)
g(b) = (println("gcall even more expensive"); b+2)
h(x,y) = x+y
then
julia> h(f(1),g(2)) # paid the price
fcall was expensive
gcall even more expensive
6
julia> h.(f.(Nullable{Int}()),g(2)) # just gcall
gcall even more expensive
Nullable{Int64}()
julia> h.(f.(Nullable{Int}()),g.(2)) # no fcall or gcall
Nullable{Int64}()
julia> h.(f.(9),g.(Nullable{Int}())) # no fcall or gcall
Nullable{Int64}()
And I can accept that broadcast may not have standard evaluation semantics, but need to wrap my head around that. Not that I was intending to program with side effects, just an academic question.
No, you need to use if manually. There’s also the difficulty that you need to know the type that f(1) would have had if it had been called (if you want to ensure type stability).
If your Nullable either isnull or contains true, then there’s no point in storing a Bool in it.
const true_flag = Nullable(nothing)
const false_flag = Nullable{Void}()
@assert typeof(false_flag) == typeof(true_flag)
bool2flag(b::Bool) = b ? true_flag : false_flag
f(a) = (println("fcall was expensive"); a+1)
pass(x, flag) = x
pass.(f.(9), false_flag) # f not called
pass.(f.(9), true_flag) # f called
maybef(b::Bool) = pass.(f.(9), bool2flag(b)) # f called iff b
No, it is identical in complexity to your code. The extra lines are strictly for readability. The below does the same thing.
f(a) = (println("fcall was expensive"); a+1)
pass(x, flag) = x
pass.(f.(9), Nullable{Void}()) # f not called
pass.(f.(9), Nullable(nothing)) # f called
maybef(flag::Bool) = pass.(f.(9), Nullable(nothing, flag)) # f called iff flag