How can I extend a primitive type

I’d like to implement a Percent type, which will be a subtype of Float64 and can utilise Float64. Ideally I’d like to something like this:

struct Percent <: Float64 
    v::Float64
    @assert 0≤v≤1 # I'm not sure if this is possible without an extra package?
end

Base.show(io::IO, p::Percent) = print(io, p*100, "%")

@test Percent(0.3)*10 == 3

I’ve read through the types docs but can only get it to work like Percent(0.3).v*10 which means I’m just using the Float anyway.

Concrete types in Julia can’t have subtypes. You probably just want to subtype Real.

This is not the sort of constraint you can easily impose in the type domain — it has to be checked at runtime — so it might not be worthwhile to implement this as a type.

For a runtime constraint like this, I would normally just define an ispercent(x::Real) = 0 ≤ x ≤ 1 function and check it as needed, maybe also defining a checkpercent(x) = ispercent(x) || throw(DomainError("$x is not in [0,1]")) function to throw errors. (You wouldn’t use @assert because assertions can be turned off — they are to check for bugs, not for users passing invalid data.)

Oscar directly answered your question. However, consider if that’s what you really want.

The way I understand it is that Percent <: Real is a real number restricted to the range [0, 1]. But then the following are valid operations:

Percent(0.8) + Percent(0.4) # 1.2
Percent(0.2) + 1000 # 1000.2

Not only their result is not a Percent anymore (which would be fine), they might be simply wrong if by “percent” you mean a proportion of something, e.g.,

probabilities_A = [Percent(3 / 4), Percent(1 / 4)]
probabilities_B = [Percent(1 / 2), Percent(1 / 2)]
probabilities_A[1] + probabilities_B[1] # You probably never want this

Therefore, if you want to use Percent as a proportion of something, it might be a good idea to keep it simple:

struct Percent
    v::Float64
    function Percent(v)
        # validate 0 <= v <= 1
        new(v)
    end
end

percent_of(p::Percent, n::Number) = p.v * n
# or maybe
import Base.*
(*)(p::Percent, n::Number) = p.v * n

Why can’t a percentage be outside the interval [0, 1]? What happens if you get a 150% pay raise? Do you say “No thank you, that’s not a percentage I’m comfortable with.”

Thanks for all your answers.

How would I subtype it? Given I’d like Percent(0.3)*2 to work? The docs suggest “Abstract types cannot be instantiated” but Real can be with Real(0.3). Clearly I’m miss-understanding something here.

Okay, so the constructor wouldn’t be doing the check? You’d have:

my_percent = 0.3
checkpercent(my_percent) || foo_requires_percent(my_percent)

Ideally rather than having foo_requires_percent I’d prefer foo(bar::Percent)

Sure.

Good point. I’m interested in how I’d do this from both a proportion point of view, or not (Percent(10)=1000% being valid)

Ideally I’d prefer to inherit these methods from the parent type given that Percent <: Real

No:

julia> typeof(Real(0.3))
Float64

Ah, so this was my miss-understanding, effectively there’s a function Real(x::Float64)=Float64(x)? Although I presume this isn’t the actual implementation.

Yes, the actual implementation is

(::Type{T})(x::T) where {T<:Number} = x

A bit hard to read, but it means that for any type T, if the type of x is a subtype of T, just return x itself.

I don’t belive this comment is very constructive, it is clearly an application specific consideration. In my scenario I’m interested in considering any percentage and

Well, excuse me. I was just pointing out that percentages are not restricted to be in any particular range, and it was not clear to me why it should be. And I was trying to be funny.

OK, I’ll stop bothering you.

You haven’t really told us what you are doing in a more complete sense, but I have a hard time believing that making a special number type is really going to be worth your trouble. The only thing that differentiates your idea of a Percent from a Float64 is the domain restriction. Just throw that @assert line everywhere and move on!

I appreciate your point if it is difficult. I presumed it would be trivial, in Python it is rather trivial:

class Percent(float):
    def __init__(self, v) -> None:
        if v > 100:
            raise ValueError("percent must be <= 100")
        if v < 0:
            raise ValueError("percent must be >= 0")
        self.value = v
>>> Percent(20)*2
40.0

Note: As already outlined the checks would be option.

Depending on where things go this would make it easier for me to extend it. For example I could make a currency type USD <: Real, now I can have to_GBP(amount::USD) and to_GBP(amount::EUR). For me these types make it possible to write cleaner function signatures and leverage multiple dispatch further. But perhaps I’m missing a point.

My point is that you perhaps should not consider Percent to be a special Real number, because it makes no sense to compute, e.g., 3% + 3.5.

Percents are usually used to express relative amounts with respect to some base quantity. In that case, it also makes no sense to compute, e.g., 100*(5/20)% + 100*(3/12)% because the base quantities (denominators) are different. Yet, 100*5/20 + 100*3/12 is a perfectly okay expression for real numbers.

So I suggest not to subtype Percent <: Real if by “percent” you mean a ratio.

If by “percent” you just mean “a real number that is between 0 and 1”, then it is probably not worth it to create a new type for that (as @tbeason said). You could just do

function percent(v) 
    0 ≤ v ≤ 1 || throw(DomainError("$v is outside [0, 1]"))
    return v
end

Lastly, if you really really wanted to create a custom numeric type, you would have to define promotion rules and conversion methods.

Check GitHub - PainterQubits/Unitful.jl: Physical quantities with arbitrary units.

No, you’d have

function foo_requires_percent(p::Real)
    checkpercent(p) # throws DomainError if p ∉ [0,1]
    ... do stuff ...
end

i.e. you’d add an argument check to every function that requires its argument to be in [0,1], but allow the caller to pass an instance of any Real type.

Your proposed alternative of foo_requires_percent(p::Percent) just forces the user to call it as foo_requires_percent(Percent(0.1)), which still does a runtime check, potentially requires code at every call site instead of once in the function, is more annoying to use, and requires a bunch of boilerplate if you want Percent to otherwise act exactly like Float64.

Thank you, these are good points you raise, and I agree percent is perhaps best to not subtype Real.

I’ve been looking through Unitful, the docs show how to dispatch on dimensions but not units? Otherwise Unitful seems like a good option for that scenario. I’ll keep looking into it.

julia> @unit USD "USD" USD 1 false
USD

julia> foo(x::USD) = x
ERROR: ArgumentError: invalid type for argument x in method definition for foo at REPL[42]:1
Stacktrace:
...

I see, this would require diligent usage of checkpercent. It is effectively extracting type checking away from the type, which as you suggest could reduce the number of calls. I appreciate this is a solution though.

My proposal was to have

foo(bar::Percent)=...
foo(bar::USD)=...
foo(bar::Float64)=...

Leveraging Julia’s multiple dispatch capabilities and gaining the benefits this provides.

Perhaps what I’m after isn’t idiomatic Julia.

No more diligent than putting ::Percent in every method where you want to impose this constraint on an argument.

In contrast, units (ala Unitful.jl) do not simply impose constraints on values — they track how units change as you pass them through operations like x^2 or x/y, detect compatibility of different combinations, and moreover they can do this statically (at compile time, so expressing information in the type domain is a big win).

It’s hard to judge what you want based on “foo”, but I suspect that this is too much punning — if foo is doing something completely different for a percentage than for a Float64, why should the two functions have the same name? And if it is doing the same thing, why can’t you write a single foo(bar::Real)?

(You don’t generally use type dispatch to differentiate behavior based on value information.)

Perhaps we are splitting hairs. I type the majority of functions I write so for me it is already engrained.

Sure, perhaps I’m being too abstract. Here is a more concrete example which I’d use multiple dispatch for:

function new_salary(current_salary::Real, change::Percent=Percent(150)
    return current_salary * (1 + change)
end

function new_salary(current_salary::Real, change::Real)
    return current_salary + change
end

@DNF :slight_smile:

I think they are similar enough that multiple dispatch isn’t too confusing.

On the contrary, I think this is a perfect case for spelling them differently. It seems a heck of a lot clearer to spell the two functions as current_salary + change and current_salary * (1 + change). :wink:

(This is the problem with trying to argue about type design in the abstract or from unrealistic toy examples.)