Usage of Nullable in function arguments


#1

I guess this is something simple, but I can’t quite understand, how to use Nullable in function arguments.
May be I am doing it wrong from the start, but I want the following: have a function which has not nullable argument or nothing on outside and Nullable in function body. Unfortunately something like this do not work

function f(x::Nullable{Int} = nothing)
  get(x, 0)
end

f(1)   # Error
f()     # Error

Of course, I can add something like this

f(x:Int) = f(Nullable(x))
f() = f(Nullable{int}())

than it works fine, but if I have a function with more than one argument (like ten for example), then number of such additional functions explode exponentially.

Requiring from end user to wrap everything in Nullable looks inconvenient to me, because

f(Nullable(5))

looks too long, but may be I am wrong.


#2

Make f an interface that does the wrapping and then calls an implementation, say faux, that takes the nullables? Just a thought.

function f(; x::Union{Int,Void} = nothing,
             y::Union{String,Void} = nothing)
    faux(x == nothing ? Nullable{Int}() : Nullable(x),
         y == nothing ? Nullable{String}() : Nullable(y))
end

function faux(x::Nullable{Int}, y::Nullable{String})
    println(isnull(x) ? "no x" : "x == $(get(x))")
    println(isnull(y) ? "no y" : "x == $(get(y))")
end

f(x = 31)
f(y = "31")
f(x = 3, y = "14")
f(x = "31") # error

#3

I would say your suggested way is the way to do it. If you want to make sure that all your arguments are wrapped in nullables, you could

function asnullable(x)
   isa(x, Nullable) ? x : Nullable(x)
end

and apply this to all your arguments as the first thing inside the function. If you want the function f to be specific about input types (e.g. Int), you can give them type Union{Int, Nullable{Int}}. But what exactly are you meaning to achieve?


#4

Thank you for your replies. I am trying to rewrite in julia some python code, which heavily uses functions like this: “def f(x=None, y=None, z=None)” where values of x, y, z either defined by user or calculated in run time. On julian side, types of arguments are known, so I thought that it would be more idiomatic to use Nullable{T} types instead of Union{Void, T}. Also, I’ve read this advice http://docs.julialang.org/en/stable/manual/style-guide/?highlight=nullable#avoid-type-unions-in-fields, but it looks that it is applicable only for type declaration, not methods.


#5

Yes, very often when you find something ‘missing’ from Julia syntax it derives from porting code written in a language with subtly different logic.
Could you consider creating the same functionality by using multiple dispatch, i.e. functions with different numbers and types of arguments? Also, is there a reason you don’t just calculate the run-time values as a default value in the function definition, e.g. f(x = 2, y = 2x, z = 3x + 2y) = x + y + z


#6

One important distinction between Python and Julia is that in Python default function arguments are evaluated when the function is defined, but in Julia they are evaluated when the function is called. That’s why @mkborregaard’s suggestion works in Julia, but wouldn’t work in Python.

As a result, I’ve found that the f(x=None) pattern is much less necessary than it was for me in Python.

But if you do want to use that pattern, there’s no performance downside to having a Union{Int, Void} argument. As you’ve seen from the docs, the performance issues with Union are all related to holding a Union within a type or Array, which is why we use Nullable in those situations.


#7

I see, it makes more sense now. Syntax with f(x = 1, y = x) really useful.

One last question, what should be done in case when function behavior depends on whether argument has some value or not defined at all? Something like this

function f(n = nothing)
  # if n is nothing then loop indefinitely, else run from 1 to n
  ...
end

In this case defining n as n::Union{Void, Int} looks somewhat excessive, and three functions

function inner_loop end

function f(n::Int)
  for i in 1:n
    inner_loop()
  end
end

function f()
  while(true)
    inner_loop()
  end
end

looks like too much boilerplate.

In types some field can be undefined and checked later with isdefined, which is exactly what I want, but it is not possible in methods.


#8

I think that implementation looks right :slight_smile:


#9

There’s nothing wrong with your 3-function implementation, and it very cleanly separates the different behaviors. It also makes it easy to add some other way to call inner_loop() in the future, and it lets Julia decide at compile-time which version of the function will be used. If I were writing this code, I would probably structure it in that way.

But, if you really want only one function definition, you can always do something like:

function f(n::Union{Int, Void}=nothing)
    if n === nothing
        while true
            inner_loop()
        end
    else
        for i in 1:n
            inner_loop()
        end
    end
end

#10

I always have to double check if this way will compute the branch at compile time, but note that type checking (in type-stable function) is always done at compile time. So if you write a conditional with if typeof(x)<:Void, it will compile into a function for which it’s true, and a function for which it’s false, depending on the type of x, so there is no runtime cost.

This works for any type except for those in keyword arguments. But the problem with keyword arguments is being addressed:


#11
function f(n::Number=Inf)
    i = 0
    while (i+=1) <= n
        inner_loop()
    end
end

#12

You don’t even need to write if typeof(x) <: Void: as can be checked e.g. by calling @code_llvm, x === nothing is also computed at compile time.