Lazy broadcast Nullable

question

#1

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.


#2

In Julia 0.6, h.(f.(Nullable{Int}()), g(2)) will directly return Nullable() without evaluating f nor h.


#3

Magic! But then doesn’t this violate function call semantics? Suppose f and g had side effects.


#4

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).


#5

You are right: updated MWE

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.


#6

A related question: is there a shortcut version of

Nullable(f(1), false)

ie one that would not evaluate the first argument when the second is false?


#7

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).


#8

I think I figured out a way to avoid specifying the type, inspired by your original solution:

f(a) = (println("fcall was expensive"); a+1)
pass(x, flag) = x
pass.(f.(9), Nullable{Bool}())  # f not called
pass.(f.(9), Nullable(true))    # f called

Just instead of true, use a flag, eg

maybef(flag) = pass.(f.(9), Nullable(flag, flag))

which looks type-stable. The logic above is not elegant, but if I design all of my functions this way (ie return Nullable’s), I can avoid it entirely.

Now I wonder if there is a simpler way, or at least something from Base I could use in place of pass.


#9

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

#10

It is basically costless, and your code is much more complicated.


#11

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