Tutorial on using advanced type system in Julia?

Success! Ignore the redundant pmap function - just a testcase. The point is that the hack results in the correct inferral. Seems like there should be a more built-in way to do this.

pmap2(fn, itr) = pmap2(fn, itr, eltype(typeof(map(fn, []))))

function pmap2(fn, itr, ::Type{T}) where T
    results = Vector{T}(undef, length(itr))
...
    return results
end

julia> @code_warntype pmap2(x::Int->x/2,[1,2])
Variables
  #self#::Core.Compiler.Const(pmap2, false)
  fn::Core.Compiler.Const(var"#350#351"(), false)
  itr::Array{Int64,1}

Body::Array{Float64,1}
1 ─ %1 = Base.vect()::Array{Any,1}
│   %2 = Main.map(fn, %1)::Array{Float64,1}
│   %3 = Main.typeof(%2)::Core.Compiler.Const(Array{Float64,1}, false)
│   %4 = Main.eltype(%3)::Core.Compiler.Const(Float64, false)
│   %5 = Main.pmap2(fn, itr, %4)::Array{Float64,1}
└──      return %5

The built-in way to do it is: Core.Compiler.return_type(fn, Tuple{Any}) (and this is likely what gets called in the end) but I think eltype(map(fn, [])) is a nice way of writing it.

(The typeof is not necessary.)

1 Like

:+1: Perfect - thanks!

A minor remark: You don’t have to specify the argument type in the definition of fn. Instead you can specify the element type of the empty array:

fn = x->x/2
itr = 1:3
eltype(map(fn, eltype(itr)[])) # Float64
itr2 = big.(1:3)
eltype(map(fn, eltype(itr2)[])) # BigFloat
3 Likes

Since first is type-stable, wouldn’t it be simpler to use something like this?

typeof(fn(first(itr)))

Also, I’m not sure that the two-stage process is needed. For example:

function pmap3(fn, itr)
    T = typeof(fn(first(itr)))
    results = Vector{T}(undef, length(itr))

    i = 1
    for x in itr
        @inbounds results[i] = fn(x)
        i+=1
    end
    return results
end

yields

julia> using Test

# "simple" case of an Array
julia> @inferred pmap3(x->x/2, [1, 2])
2-element Array{Float64,1}:
 0.5
 1.0

# More complex case with a generator
julia> @inferred pmap3(x->x/2, (1//i for i in 1:2))
2-element Array{Rational{Int64},1}:
 1//2
 1//4

However, this does not seem to work when type instabilities are involved:

julia> @inferred pmap3(x->x%2==0 ? 0 : 0.1, [1, 2])
ERROR: return type Array{Float64,1} does not match inferred return type Array{_A,1} where _A
Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] top-level scope at REPL[5]:1
2 Likes

No, it will not (yet) infer a general result type that is a Union.
However, typeof will abstract:

julia> typeof( map(x->x%2==0 ? 0 : 0.1, [1, 2]) )
Array{Real,1}

julia> typeof( map(x->x%2==0 ? 0.0f0 : 0.1, [1, 2]) )
Array{AbstractFloat,1}

You are correct. I noticed the same thing a little after the above posting and folded it in.

This is cute! I’ll go with this.

Actually, I think it still does (or is at least the same). The error you are seeing is the @inferred macro (properly) complaining about type stability. I get the same result with the longer map version.

Two quick things:

  • Because of some quirky compiler trade-offs, pmap2(fn, itr) = pmap2(fn, itr, eltype(typeof(map(fn, [])))) will keep fn “generic”, and will not specialize on it. Use pmap2(fn::F, iter) where F = ... Function and DataTypes are the only objects treated this way.
  • Instead of calling return_type, consider whether your problem can’t be solved with type widening via GitHub - JuliaFolds/BangBang.jl: Immutables as mutables, mutables as immutables.
1 Like

Is there a tutorial on advanced use of types in Julia? The manual isn’t enough for me to read advanced abstract type usage in actual packages.

I think Quantitative Economics with Julia might be helpful who would like to study Julia, especially type system.

https://julia.quantecon.org/index_toc.html