Promote failed to change any arguments

I am having trouble figuring out how to implement promotions for a type. The main idea is the create a class that tracks operations applied to a variable type. This is perhaps not so different from Flux, but I wanted to try implementing it from first principles before I try using a library. However, I cannot make this promote_rule work to allow the two types to operate together:

abstract type RV end

struct ArrayRV <: RV
    variates
end

struct NExpr{N}
    rvs::NTuple{N, RV}
    f
end

apply(f, rvs::RV...) = NExpr(rvs, f)

Base.convert(::Type{NExpr}, x::RV) = apply(x -> x, x)
Base.promote_rule(::Type{NExpr{N}}, ::Type{RV}) where N = NExpr{N}

x = ArrayRV([1, 2, 3])
y = apply(x -> 2*x, x)
promote(x, y)

Produces the following error:

julia> promote(x,y)
ERROR: promotion of types ArrayRV and NExpr{1} failed to change any arguments
Stacktrace:
 [1] sametype_error(::Tuple{ArrayRV,NExpr{1}}) at .\promotion.jl:308
 [2] not_sametype(::Tuple{ArrayRV,NExpr{1}}, ::Tuple{ArrayRV,NExpr{1}}) at .\promotion.jl:302
 [3] promote(::ArrayRV, ::NExpr{1}) at .\promotion.jl:285
 [4] top-level scope at none:0

The docs show some examples for Rational, but I must have missed something. Where should I look next?

https://docs.julialang.org/en/v1/manual/conversion-and-promotion/

Julia’s promotion machinery works only for subtypes of Number.

What are you trying to do?

I see. Then here’s the more thorough explanation. I want to create a random variable type. The idea is that applying operators to the random variable type creates expressions which can be sampled from using Monte Carlo simulation. Essentially I’d like to be able to replace numerical variables in a formula and easily create Monte Carlo simulations from standard formulas.

I hadn’t thought to subtype Number, although maybe that’s appropriate. I want to treat them like numbers in most cases. Would that be a reasonable route to go?

Regarding the original question: you should just extend the definitions to allow subtypes (since the methods are called with a concrete type):

Base.convert(::Type{<:NExpr}, x::RV) = apply(x -> x, x)
Base.promote_rule(::Type{NExpr{N}}, ::Type{<:RV}) where N = NExpr{N}

This now works with your example.

I am not sure why you say this, the code is generic.

I would not subtype Number; just define the operations you want to perform. The promotion machinery can help with that, but that’s just a convenience feature to organize your code.

2 Likes

Yeah–promotion works with any types, not just subtypes of Number, but it can be easier to use promotion and operator overloading with Number subtypes because Julia defines some useful methods like these:

+(x::Number, y::Number) = +(promote(x,y)...)
*(x::Number, y::Number) = *(promote(x,y)...)
-(x::Number, y::Number) = -(promote(x,y)...)
/(x::Number, y::Number) = /(promote(x,y)...)

source

You can define those same operations for any type you create, although subtyping Number can save you some work because of convenience methods like those.

Thanks @Tamas_Papp, I definitely forgot about using subtypes.

Since most of the operators are just straight forward to wrap in my modules apply function, I was originally planning to just have a for-loop that iterates over the standard operators and use @eval to create the default definitions.

I figure as I work through some specific scenarios I may decide that certain operations can benefit from special handling. For now I’m mostly trying to setup a minimal API to see if it feels right.

1 Like