To use or not to use promote_op

According to Output type in the manual I should use promote_op to decide container types. But according to the output of help?> Base.promote_op I should avoid promote_op as it is “fragile”. And if I did use promote_op is the resulting function performant or do I need to define some new inner functions (for function barriers)?

Another style is function foo( T, cont1, cont2 )..., or, should I define it as function foo( ::Val{T}, cont1, cont2 ) where{T}...?

I have read the “Performance Tips” section multiple times as well as “Design Patterns with Parametric methods” but I am still quite confused.
Thanks for any advice.
–shiv–

Using @code_warntype it seems a good way to organize the code is as follows:

function foo( T, x, y )
 acc = zero( T )
 for i in 1:length(x)
  acc += T( x[i] * y[i] )
 end
 return acc
end

Here I pass the output type explicitly as the first argument. In case the user “messes up” and passes a lesser type than what promote_op would have chosen it is good to explicitly convert the summand to acc, hoping that LLVM will discard unneeded type conversions as no-ops. Declaring acc :: T only seems to insert unnecessary type assertions.

I also noticed that replacing T with ::Val{T} as the first argument seemed to have no effect on code generation but makes the interface a bit more complex to use.

I am hoping that these observations will generalize to more complex situations…
–shiv–

I cannot comment on promote_op, but the above should be foo(::Type{T},

Could be. I just pulled ::Val{T} from Value Types in the manual.

The Type{T} annotation forces type specialization, otherwise it’s equivalent to writing foo(T::DataType, ... which will treat all types the same, as values of type DataType.

It’s possible Val{T} can achieve something similar (never tried that), but you would need to call the function with Val(T).

Following @DNF’s suggestion I also tried

function foo( ::Type{T}, x, y ) where T
 acc = zero( T )
 for i in 1:length(x)
  acc += T( x[i] * y[i] )
 end
 return acc
end

and looking at the output of @code_warntype this seems to be a better solution.

And for the sake of completeness code with promote_op:

function foo( x, y )
 op( a, b ) = a*b + a*b
 T = Base.promote_op( op, eltype(x), eltype(y) )
 acc = zero( T )
 for i in 1:length(x)
  acc += x[i] * y[i]
 end
 return acc
end

It could also be an alternative to peel off the first element of each array to correctly initialize the accumulator.

I avoided initializing acc = x[1]*y[1] mostly because my container types are quite nested and figuring out the first element is a bear.

In any case, thinking it over now, I see that it is more flexible to let the user choose the output type.

promote_op is used in many places in Base, so lots of functions rely on it already.
If specifying the resulting type manually is sometimes (but not always) useful, you can nicely do

foo(x, y) = foo(promote_op(*, eltype(x), eltype(y)), x, y)  # determine T automatically
foo(::Type{T}, x, y) = ... use T type ...

Oh that is a fantastic suggestion! Thanks.