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.)
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:
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
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
0 ≤ v ≤ 1 || throw(DomainError("$v is outside [0, 1]"))
checkpercent(p) # throws DomainError if p ∉ [0,1]
... do stuff ...
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.
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)
function new_salary(current_salary::Real, change::Real)
return current_salary + change