ERROR: MethodError: no method matching decompose()

Hi,

I have defined the following types:

struct Percentage <: Real
    value::Float64
    Percentage(x) = x < 0 ? new(0) : x > 1 ? new(1) : new(round(x, digits = 6))
end

Base.show(io::IO, x::Percentage) = print(io, "$(x.value * 100)%")

Base.convert(::Type{Percentage}, x::Real) = Percentage(x)
Base.convert(::Type{Percentage}, x::Percentage) = x

Base.promote_rule(::Type{T}, ::Type{Percentage}) where T <: Real = Percentage

import Base: +, -, *, /, <, >, <=, >=, ==

+(x::Percentage, y::Percentage) = Percentage(x.value + y.value)
-(x::Percentage) = Percentage(-x.value)
-(x::Percentage, y::Percentage) = Percentage(x.value - y.value)
*(x::Percentage, y::Real) = Percentage(x.value * y)
/(x::Percentage, y::Real) = Percentage(x.value / y)
<(x::Percentage, y::Percentage) = x.value < y.value
<=(x::Percentage, y::Percentage) = x.value <= y.value
>(x::Percentage, y::Percentage) = x.value > y.value
>=(x::Percentage, y::Percentage) = x.value >= y.value
==(x::Percentage, y::Percentage) = x.value == y.value

Base.max(x::Percentage, y::Percentage) = Percentage(max(x.value, y.value))
Base.min(x::Percentage, y::Percentage) = Percentage(min(x.value, y.value))


mutable struct Health <: Real
    current::Percentage
    Health(current=1) = new(current)
end

Base.convert(::Type{Health}, x::Real) = Health(x)
Base.convert(::Type{Health}, x::Health) = x

Base.promote_rule(::Type{T}, ::Type{Health}) where T <: Real = Health
Base.promote_rule(::Type{Health}, ::Type{Percentage}) = Health

import Base: +, -, *, /, <, >, <=, >=, ==

+(x::Health, y::Health) = Health(x.current + y.current)
-(x::Health) = Health(-x.current)
-(x::Health, y::Health) = Health(x.current - y.current)
*(x::Health, y::Real) = Health(x.current * y)
/(x::Health, y::Real) = Health(x.current / y)
<(x::Health, y::Health) = x.current < y.current
<=(x::Health, y::Health) = x.current <= y.current
>(x::Health, y::Health) = x.current > y.current
>=(x::Health, y::Health) = x.current >= y.current
==(x::Health, y::Health) = x.current == y.current

Base.max(x::Health, y::Health) = Health(max(x.current, y.current))
Base.min(x::Health, y::Health) = Health(min(x.current, y.current))

When I run the following code

h = Health(0.3)
h.current += Health(0.1)

I get the following error:

ERROR: MethodError: no method matching decompose(::Health)

I’m guessing it has to do something with the promote rules. Is there a way to fix this? I’m not using that line of code literally; in the real code it looks like:

h.current += x

where x has been assigned a value of type Health.

Thanks in advance,
Stef

Adding

Base.convert(::Type{Percentage}, x :: Health) = x.current

does the trick. But h += Health(0.1) feels more logical anyway.

I can’t help feeling that this is a lot of boilerplate code for what it does. One way of reducing the boilerplate is

for op = (:+, :-)
    eval(quote
        Base.$op(a::Health, b::Health) = Health($op(a.current, b.current))
    end)
end

and similar (see the docs on code generation.

1 Like

Thanks for the trick to reduce the boiler plate code!

If I add that line I get the following:

ERROR: StackOverflowError:
Stacktrace:
 [1] *(::Float64, ::Health) at ./promotion.jl:312
 [2] *(::Percentage, ::Health) at /Users/stef/Programming/Julia Projects/loreco-abm/cockpit/utilities/percentage.jl:18
 [3] *(::Health, ::Health) at /Users/stef/Programming/Julia Projects/loreco-abm/cockpit/utilities/health.jl:20
 ... (the last 3 lines are repeated 26660 more times)

Maybe I need to add the code I’m actually executing:

using Main.Types

abstract type Lifecycle end

struct Restorable <: Lifecycle
    health::Health
    damage_thresholds::Vector{Tuple{Percentage, Float64}}
    restoration_thresholds::Vector{Tuple{Percentage, Float64}}
    wear::Float64
    Restorable(
        health=1;
        damage_thresholds=[(1, 1)],
        restoration_thresholds=[(1, 1)],
        wear=0) = new(health,
                    complete(damage_thresholds),
                    complete(restoration_thresholds),
                    wear)
end

function complete(thresholds::Vector)
    thresholds = sort(thresholds)

    if length(thresholds) == 0
        push!(thresholds, (1, 1))
    elseif thresholds[end][1] != 1
        push!(thresholds, (1, thresholds[end][2]))
    end

    return thresholds
end

@enum Direction up down

function change_health(lifecycle::Lifecycle, change::Real, direction::Direction)
    if direction == up
        thresholds = lifecycle.restoration_thresholds
    else
        thresholds = lifecycle.damage_thresholds
    end

    # It's easy when there is only the 100% threshold
    if length(thresholds) == 1
        real_change = thresholds[1][2] * change
        surplus_change = nothing
    else
        multiplier = nothing
        max_change = nothing
        index = length(thresholds) - 1

        while index > 0 && multiplier == nothing
            if lifecycle.health > thresholds[index][1]
                multiplier = thresholds[index + 1][2]

                if direction == up && index + 1 < length(thresholds)
                    # No surplus change can happen above 100%
                    max_change = thresholds[index + 1][1] - lifecycle.health
                elseif direction == down
                    max_change = lifecycle.health - thresholds[index][1]
                end
            end

            index -= 1
        end

        if multiplier == nothing
            # Health is below lowest threshold
            real_change = thresholds[1][2] * change
        else
            real_change = change * multiplier
        end

        if max_change != nothing && real_change > max_change
            surplus_change = (real_change - max_change) / multiplier
            real_change = max_change
        else
            surplus_change = nothing
        end
    end

    if direction == up
        lifecycle.health.current += real_change
    else
        lifecycle.health.current -= real_change
    end

    return change_health(lifecycle, surplus_change, direction)
end

function change_health(lifecycle::Lifecycle, change::Nothing, direction::Direction)
    return lifecycle
end

function use!(lifecycle::Restorable)
    damage!(lifecycle, lifecycle.wear)
end

function damage!(lifecycle::Lifecycle, damage::Real = 0.01)
    change_health(lifecycle, damage, down)
end

function restore!(lifecycle::Lifecycle, damage::Real = 0.01)
    change_health(lifecycle, damage, up)
end

The problem occurs when the following lines, at the end of the change_health() function, are executed:

 if direction == up
        lifecycle.health.current += real_change
    else
        lifecycle.health.current -= real_change
    end

In psedocode, the infinite recursion is as follows:

  • Evaluate Float64 * Health

  • Promote Float64 to Health, and evaluate Health * Health

  • Health is a subtype of Real, so apply *(x::Health, y::Real) = Health(x.current * y), and evaluate x.current * y

  • x.current is a Percentage, and y is a Health is a Real, so apply *(x.current::Percentage, y::Real) = Percentage(x.current.value * y), and evaluate x.current.value * y

  • x.current.value is a Float64, and we’re back where we started.

Since values have to be Float64, you could likely define *(::Health, ::Float64) instead of *(::Health, ::Real) without breaking anything. That would prevent the recursion. But I think you’re fighting against the design intent of Julia by making Health a subtype of Real, and you might run in to other problems.

2 Likes

Thanks @thisrod! That got me thinking. I eventually solved the problem by redefining the types and managed to compact the code significantly thanks to the suggestion from @hendri54.

module Types

export Percentage, Health

struct Percentage <: Real
    value::Float64
    Percentage(x) = x < 0 ? new(0) : x > 1 ? new(1) : new(round(x, digits = 6))
end

Base.show(io::IO, x::Percentage) = print(io, "$(x.value * 100)%")

Base.convert(::Type{Percentage}, x::Real) = Percentage(x)
Base.convert(::Type{Percentage}, x::Percentage) = x

Base.promote_rule(::Type{T}, ::Type{Percentage}) where T <: Real = Percentage

mutable struct Health
    current::Percentage
    Health(current=1) = new(current)
end

value(x::Percentage) = x.value
value(x::Health) = value(x.current)

import Base: +, -, *, /, <, >, <=, >=, ==, max, min

for type in (Percentage, Health)
    for op in (:+, :-, :max, :min)
        eval(quote
            Base.$op(x::$type, y::$type) = $type($op(value(x), value(y)))
            Base.$op(x::$type, y::Real) = $type($op(value(x), y))
            Base.$op(x::Real, y::$type) = $type($op(x, value(y)))
        end)
    end

    for op in (:*, :/)
        eval(quote
            Base.$op(x::$type, y::Real) = $type($op(value(x), y))
            Base.$op(x::Real, y::$type) = $type($op(x, value(y)))
        end)
    end

    for op in (:<, :<=, :>, :>=)
        eval(quote
            Base.$op(x::$type, y::$type) = $op(value(x), value(y))
            Base.$op(x::$type, y::Real) = $op(value(x), y)
            Base.$op(x::Real, y::$type) = $op(x, value(y))
        end)
    end
    eval(quote
        ==(x::$type, y::$type) = value(x) == value(y)
        ==(x::$type, y::Real) = value(x) == y
        ==(x::Real, y::$type) = x == value(y)
    end)
end

end

Funny - it runs for me:

struct Percentage <: Real
    value::Float64
    Percentage(x) = x < 0 ? new(0) : x > 1 ? new(1) : new(round(x, digits = 6))
end

Base.show(io::IO, x::Percentage) = print(io, "$(x.value * 100)%")

Base.convert(::Type{Percentage}, x::Real) = Percentage(x)
Base.convert(::Type{Percentage}, x::Percentage) = x

Base.promote_rule(::Type{T}, ::Type{Percentage}) where T <: Real = Percentage

import Base: +, -, *, /, <, >, <=, >=, ==

+(x::Percentage, y::Percentage) = Percentage(x.value + y.value)
-(x::Percentage) = Percentage(-x.value)
-(x::Percentage, y::Percentage) = Percentage(x.value - y.value)
*(x::Percentage, y::Real) = Percentage(x.value * y)
/(x::Percentage, y::Real) = Percentage(x.value / y)
<(x::Percentage, y::Percentage) = x.value < y.value
<=(x::Percentage, y::Percentage) = x.value <= y.value
>(x::Percentage, y::Percentage) = x.value > y.value
>=(x::Percentage, y::Percentage) = x.value >= y.value
==(x::Percentage, y::Percentage) = x.value == y.value

Base.max(x::Percentage, y::Percentage) = Percentage(max(x.value, y.value))
Base.min(x::Percentage, y::Percentage) = Percentage(min(x.value, y.value))


mutable struct Health <: Real
    current::Percentage
    Health(current=1) = new(current)
end

Base.convert(::Type{Health}, x::Real) = Health(x)
Base.convert(::Type{Health}, x::Health) = x
Base.convert(::Type{Percentage}, x :: Health) = x.current

Base.promote_rule(::Type{T}, ::Type{Health}) where T <: Real = Health
Base.promote_rule(::Type{Health}, ::Type{Percentage}) = Health

import Base: +, -, *, /, <, >, <=, >=, ==

# +(x::Health, y::Health) = Health(x.current + y.current)
-(x::Health) = Health(-x.current)
# -(x::Health, y::Health) = Health(x.current - y.current)
*(x::Health, y::Real) = Health(x.current * y)
/(x::Health, y::Real) = Health(x.current / y)
<(x::Health, y::Health) = x.current < y.current
<=(x::Health, y::Health) = x.current <= y.current
>(x::Health, y::Health) = x.current > y.current
>=(x::Health, y::Health) = x.current >= y.current
==(x::Health, y::Health) = x.current == y.current

for op = (:+, :-)
    eval(quote
        Base.$op(a::Health, b::Health) = Health($op(a.current, b.current))
    end)
end

Base.max(x::Health, y::Health) = Health(max(x.current, y.current))
Base.min(x::Health, y::Health) = Health(min(x.current, y.current))

h = Health(0.3)
h.current += Health(0.4)

No stack overflow. No error.

That looks neater, and it fixes the problem for Health.

What happens when you multiply two percentages? I expect you’ll get an ambiguity error, because the method dispatcher could treat either argument as a Percentage and the other as a Real. The error message will suggest how to fix that.

2 Likes

That made me think :slight_smile:. The way I’m using these types it actually doesn’t make sense to multiply them but I guess I should not force that logic on everyone who would want to use these types. So I added the * and / operators to the first loop so that there are now * and / operators which take two Health and two Percentage types. I’ll leave up to the context in which these are used whether it makes sense or not.

What version of Julia are you running? I was using 1.5.2 at the time I posted. Upgraded to 1.5.3 now but haven’t tested that code anymore since I did a complete overhaul.

I’m on 1.5.2 (MacOS)