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.
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.)
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.”
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:
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.
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.
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
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.
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
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).
(This is the problem with trying to argue about type design in the abstract or from unrealistic toy examples.)