F(x) = x::Int64 or f(x)::Int64 = x?

If I want to define a function that always returns the same type, is it better to typeassert the returned value before returning it as in f(x) = x::Int64? Or is it better to convert what is returned to a desired type as in f(x)::Int64 = x? I have only seen the latter used in practice, but silently calling convert seems equivalent to crossing your fingers and hoping for the best. But I can also see that it can be convenient. Is this just a tradeoff people live with?

This is a convert and a typeassert on the return value. The latter is still necessary because convert(T, x) and the often underlying T(x) isn’t guaranteed a return type T.

If you don’t convert, the method will throw an error for all non-Int64 x. In this identity-like case where the return value is the input value, f(x::Int64) = x is a better guarantee by lacking methods for non-Int64 inputs, but typeassert alone could be useful for non-inputs, including the return value. It’s just more often the case that type-unstable methods could use a convert to standardize the return type, even generically, so the syntax was reserved for that. Either way is pretty rare though, type inference and type stability practices are pretty solid, off the top of my head I’ve really only needed to annotate fields, captured reassignable variables, a some type parameters.

julia> f(x) = x::Int64
f (generic function with 1 method)

julia> @code_warntype f(1)
MethodInstance for f(::Int64)
  from f(x) in Main at REPL[1]:1
Arguments
  #self#::Core.Const(f)
  x::Int64
Body::Int64
1 ─ %1 = Core.typeassert(x, Main.Int64)::Int64
└──      return %1


julia> f(x)::Int64 = x
f (generic function with 1 method)

julia> @code_warntype f(1)
MethodInstance for f(::Int64)
  from f(x) in Main at REPL[3]:1
Arguments
  #self#::Core.Const(f)
  x::Int64
Body::Int64
1 ─ %1 = Main.Int64::Core.Const(Int64)
│   %2 = Base.convert(%1, x)::Int64
│   %3 = Core.typeassert(%2, %1)::Int64
└──      return %3

Also worth mentioning that the compiler gets rid of convert and typeassert steps, visible in @code_warntype, if it can prove that the variable was the target type to begin with, so you don’t need to manually do so to save runtime:

julia> @code_llvm f(1)
;  @ REPL[1]:1 within `f`
define i64 @julia_f_417(i64 signext %0) #0 {
top:
  ret i64 %0
}
2 Likes

Makes sense. One consideration is that, if you want a function to return a value of a custom type, and you want to use the “declaration” syntax, you want to have convert defined for your custom type.

1 Like