Return Type Annotations

What?

Type Assertion

x = 4::Int

"this value must be an Int"

Type Annotation

x::Int = 4

"this variable must contain an Int"

Return Type Annotation

function x_func()::Int
    return 4
end

"this method must return an Int"

Can depend on types

# base/complex.jl
function ^(z::Complex{T}, p::Complex{T})::Complex{T} where T<:AbstractFloat
    if p == 2 #square
        zr, zi = reim(z)
        x = (zr-zi)*(zr+zi)
        y = 2*zr*zi
        if isnan(x)
            if isinf(y)
                x = copysign(zero(T),zr)
            elseif isinf(zi)
                x = convert(T,-Inf)
            elseif isinf(zr)
                x = convert(T,Inf)
            end
        elseif isnan(y) && isinf(x)
            y = copysign(zero(T), y)
        end
        Complex(x,y)
    elseif z!=0
        if p!=0 && isinteger(p)
            rp = real(p)
            if rp < 0
                return power_by_squaring(inv(z), convert(Integer, -rp))
            else
                return power_by_squaring(z, convert(Integer, rp))
            end
        end
        exp(p*log(z))
    elseif p!=0 #0^p
        zero(z) #CHECK SIGNS
    else #0^0
        zer = copysign(zero(T),real(p))*copysign(zero(T),imag(z))
        Complex(one(T), zer)
    end
end

Can depend on functions of types

# base/bool.jl
function *(x::Bool, y::T)::promote_type(Bool,T) where T<:Unsigned
    return ifelse(x, y, zero(y))
end

Can depend on functions of arguments

# base/random.jl
function nth(iter, n::Integer)::eltype(iter)
    for (i, x) in enumerate(iter)
        i == n && return x
    end
end

Applies to any and all return statements

How?

Magic!

aka lowering

julia> foo()::Int = 3
foo (generic function with 1 method)

julia> @code_typed foo()
CodeInfo(:(begin
        return 3
    end))=>Int64

julia> @code_lowered foo()
CodeInfo(:(begin
        nothing
        return (Core.typeassert)((Base.convert)(Main.Int, 3), Main.Int)
    end))

becomes

foo()::Int = 3
foo() = convert(Int, 3)::Int

Why?

Why?

  • Compiler hints
# src/JuMP.jl
# Returns the number of rows used by SDP constraints in the
# MPB conic representation (excluding symmetry constraints)
#   Julia seems to not be able to infer the return type
#   (probably because c.terms is Any) so getNumSDPRows tries 
#   to call zero(Any)... Using ::Int solves this issue
function getNumRows(c::SDConstraint)::Int
    n = size(c.terms, 1)
    (n * (n+1)) ÷ 2
end

Why?

  • Compiler hints
  • Simplifying output conversions
# base/hashing2.jl
function decompose(x::BigFloat)::Tuple{BigInt, Int, Int}
    isnan(x) && return 0, 0, 0
    isinf(x) && return x.sign, 0, 0
    x == 0 && return 0, 0, x.sign
    s = BigInt()
    s.size = cld(x.prec, 8*sizeof(GMP.Limb)) # limbs
    b = s.size * sizeof(GMP.Limb)            # bytes
    ccall((:__gmpz_realloc2, :libgmp), Void, (Ptr{BigInt}, Culong), &s, 8b) # bits
    ccall(:memcpy, Ptr{Void}, (Ptr{Void}, Ptr{Void}, Csize_t), s.d, x.d, b) # bytes
    s, x.exp - 8b, x.sign
end
# this slide
function add{T<:Number,S<:Number}(x::Nullable{T}, y::Nullable{S})::Nullable{promote_type(T, S)}
    if !isnull(x) && !isnull(y)
        get(x) + get(y)
    else
        Nullable()
    end
end

Why?

  • Compiler hints
  • Simplifying output conversions
  • Documentation!
"""
    Filter

I wrapper around a function that takes a log `Record` and returns
a bool whether to skip logging it.

# Fields
`f::Function`: a function that should return a bool given a `Record`
"""
immutable Filter
    f::Function
end

function (filter::Filter)(rec::Record)::Bool
    return filter.f(rec)
end

Let's get creative!

using Base.Test

# trick the compiler into allowing us to construct a type which
# dispatches as Number but has a distinct name
const Squared = Union{<:Number, <:Number}

Base.convert(::Type{Squared}, x::Number) = x * x

add(x, y)::Squared = x + y

@test add(2, 2) == 16

Maybe not that creative...

ResultTypes.jl

By Eric Davies

That's me!

Result{T, E<:Exception}
  • value-or-error type for Julia
  • big performance gains by avoiding try-catch
  • uses return type annotations to look nice!
const DivResult = Result{Int, DivideError}

function integer_division(x::Int, y::Int)::DivResult
    if y == 0
        return DivideError()
    else
        return div(x, y)
    end
end
function func1(x,y)
   local z
   try
       z = div(x,y)
   catch e
       z = 0
   end

   return z
end
function func2(x, y)
   r = integer_division(x,y)
   if iserror(r)
       return 0
   else
       return unwrap(r)
   end
end
function func1(x,y)
   local z
   try
       z = div(x,y)
   catch e
       z = 0
   end

   return z
end
julia> t2 = @benchmark for i = 1:10
                  func2(3, i % 2)
              end
BenchmarkTools.Trial:
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     98.952 ns (0.00% GC)
  median time:      99.782 ns (0.00% GC)
  mean time:        106.640 ns (0.00% GC)
  maximum time:     1.519 μs (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     925
function func2(x, y)
   r = integer_division(x,y)
   if iserror(r)
       return 0
   else
       return unwrap(r)
   end
end
julia> t1 = @benchmark for i = 1:10
                  func1(3, i % 2)
              end
BenchmarkTools.Trial:
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     321.802 μs (0.00% GC)
  median time:      330.396 μs (0.00% GC)
  mean time:        376.953 μs (0.00% GC)
  maximum time:     7.356 ms (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     1

By Using DivResult:

  • We allocate the same amount of memory (none)
  • We take 0.03% of the time!

worth it

But Wait!

  • We took the average case
  • What if we never expect an error?
    • ResultTypes used to do worse, but now...
function func1(x,y)
   local z
   try
       z = div(x,y)
   catch e
       z = 0
   end

   return z
end
julia> t2 = @benchmark for i = 1:2:20
                  func2(3, i % 2)
              end
BenchmarkTools.Trial:
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     180.738 ns (0.00% GC)
  median time:      182.029 ns (0.00% GC)
  mean time:        200.481 ns (0.00% GC)
  maximum time:     1.426 μs (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     695
function func2(x, y)
   r = integer_division(x,y)
   if iserror(r)
       return 0
   else
       return unwrap(r)
   end
end
julia> t1 = @benchmark for i = 1:2:20
                  func1(3, i % 2)
              end
BenchmarkTools.Trial:
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     252.695 ns (0.00% GC)
  median time:      256.705 ns (0.00% GC)
  mean time:        280.556 ns (0.00% GC)
  maximum time:     2.049 μs (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     390

By Using DivResult:

  • We allocate the same amount of memory (none)
  • We take 70% of the time!

still worth it

ResultTypes.jl caveat

We gain speed by avoiding tracebacks...

so we lose tracebacks

Takeaways

  • Use return type annotations for:
    • Compiler hints
    • Simplifying output conversions
    • Documentation
  • Also maybe try ResultTypes.jl

Eric Davies @iamed2

Using Return Type Annotations Effectively

By Eric Davies

Using Return Type Annotations Effectively

  • 1,734