What's Up With That PERCENT! Datatype?

So the PERCENT! datatype didn't exist in Rebol2:

rebol2>> 10%
** Syntax Error: Invalid integer -- 10%

But it was added to R3-Alpha, and then included in Red. Both of which seem to believe that percents are actually just a rendering of an equivalent decimal number, divided by 100:

r3-alpha/red>> to percent! 10
== 1000%

r3-alpha/red>> to percent! 10.0
== 1000%

r3-alpha>> to decimal! 10%
== 0.1

red>> to float! 10%  ; Red calls decimal "float", but same difference
== 0.1

This shows a kind of mathematical invariant in R3-Alpha, e.g. that 10% will act like decimal 0.1 in math... so that's what it "is":

r3-alpha>> 2 * 10%
== 0.2

r3-alpha>> 10% * 2
== 0.2

Red of course throws in a curve ball... it will either double the percent or apply it to the non-percent, depending on the order... :roll_eyes:

red>> 10% * 2
== 20%

red>> 2 * 10%
== 0.2

How about some other math...?

r3-alpha>> 10% * 10%
== 1.0000000000000002%

red>> 10% * 10%
== 1%

r3-alpha>> 10% + 2
== 2.1  ; same for 2 + 10%

red>> 10% + 2
== 210%

red>> 2 + 10%
== 2.1

Thoughts On That TO Conversion

One thing is that if we follow my "useless" reversible model of TO conversions to percent! 10 should fairly clearly be 10%.

>> to percent! 10
== 10%

>> to integer! 10%
== 10

>> to text! 10%
== "10"

>> to percent! "10"
== 10%

>> to block! 10%
== [10]

I have explained why I have been promoting this perhaps-strange idea. You get "if ten were a block, what would it look like"... "if ten were a percent, what would it look like"... with an emphasis on "look like".

Consider the challenge question: how would you turn a variable containing the integer 10 into 10% in R3-Alpha or Red today?

red>> num: 10

red>> what-operation-here num 
== 10%

Your answers are things like:

red>> to percent! num / 100
== 10%

red>> load rejoin [num "%"]
== 10%

red>> num * 1%
== 10%

But I think there should be an operation that just does that. In the system I've described, TO seems to fill this need, in a way that you can predict. Of course, that means TO is not the end-all be-all operation, and I've discussed how breaking operations out into things like JOIN and ROUND and FORM can step in for other intents.

One operation that might be an example of something that could fill in a gap here could be AS. AS allows us to ask "what if the memory or guts of the thing we're looking at were viewed through a different lens"?

>> as binary! "ABC"
== #{414243}

>> as text! #{414243}
== "ABC"

>> as decimal! 10%
== 0.1

>> as percent! 0.1
== 10%

It's one possibility of an operator that wouldn't be constrained by the equivalence classes of TO.

What About That Math?

Red's policy of making the result match the first operand feels confusing. It seems easier if there's some pecking order of type promotions, and you get the same result regardless of order. (Of course, things like matrix multiplication throw a wrench in the idea that you can say multiplication is always commutative... so, exceptions exist. Unless you do like Python and make matrix multiplication a separate operator, and reserve * for element-wise multiplication.)

Regarding picking behaviors: percent is a pretty strange situation, because there are of course different questions:

  • What happens when you double 20% ?
  • What is 20% of 2?

My default mindset would be to imagine that multiplying 20% times 2 is meant to double it, and produce 40% (which Red half agrees with). If I expected to actually take 20% of 2, I wouldn't think it unreasonable to be asked to first convert the percent to a decimal...

>> 20% * 2
== 40%

>> 2 * 20%
== 40%

>> (as decimal! 20%) * 2
== 0.4

>> 2 * (as decimal 20%)
== 0.4

Furthermore...from a perspective of dimensional analysis, I would say you shouldn't be able to do things like:

>> 20% + 1
** Error: Can't add PERCENT! and INTEGER!

But if you could do it, I would probably lean to it being more generally useful to give back 21% than 120%...which seems to be a behavior straight out of the annals of improbable usefulness.

I don't really use the percent type, but it seems to me there's simply too much guessing what people want out of it. I imagine that it best serves its purpose as just kind of a source-level lexical convenience, and your code has to decide when it wants to convert it to a decimal for the purposes of some kind of measurement.

Red's first answer here makes sense to me, the second does not--I don't see the order as distinguishing "give me half of 10 percent" and the second "give me 10% of 0.5":

red>> 10% * 0.5
== 5%

red>> 0.5 * 10%
== 0.05

So my plan is to pretty much pare down the percent type to bare bones, try to make the math commutative, and favor the idea that you have to explicitly convert a percentage to a decimal to get it to behave like a decimal... probably via the AS operator, doing the TO operator as described.

In framing how I think of the type, I see something like 10% as carrying a unit... as if it were 10pct (similar to 10px or 10in).

It doesn't seem to me that units should be dropped by ordinary math operations.

One compromise might be that if a function takes a decimal! but does not take a percent!, that the system would be willing to automatically convert the percent to its decimal equivalent as a convenience.

(A similar argument might be made that a function that takes a decimal! but doesn't take integer! would be willing to convert 1 to 1.0 in that call.)

But generally speaking, I feel math shouldn't mix units or discard them, and percent is a "unit".

I agree with this design. From my perspective as a physicist, I really hate systems which try to get clever with percentages. Theyā€™re just too ambiguous ā€” as you note, everything has multiple interpretations, and in this situation it is simply impossible to create a coherent system which ā€˜does what I meanā€™ in all circumstances.

In fact I would suggest going furtherā€¦ if someone tries to combine percentages and decimals in a single operation, that should be an error. Percentages should be convertible only to the decimal fraction they are an alternate representation of ā€” ā€˜convertingā€™ 10% to 10 makes as little sense to me as does ā€˜convertingā€™ 0.1 to 10. (For more on this see below; also I donā€™t like the as vs to distinction.) Perhaps also add new functions specifically for doing addition and subtraction of percentages from a base number, say add-perc and sub-perc. These could even be made infix, so you can write 2 add-perc 10%.

Basically, what Iā€™m getting at is that percentages are annoying and easy to misuse, so make the correspondences as straightforward and difficult to misuse as possible.

On the other hand: to me, the symbol % is nothing more than a synonym for Ć·100 (or Ɨ10ā»Ā² if you prefer). This works in much the same way as, say, a prefix like m being a synonym for Ɨ10ā»Ā³ (or Ć·1000), so mg are really Ɨ10ā»Ā³ g. Or, for that matter, Ā° being a synonym for ƗĻ€/180 ā€” which, despite being slightly strange the first time itā€™s encountered, remains the most coherent way to define degrees as a unit of angle). Thus, 20% == 0.2 is not a case of ā€˜discardingā€™ units: itā€™s simply that both sides of the equality are unitless to start off with.

From which perspective:

Itā€™s worth noting that these are the same number: 20% == 0.2. (So yes, for once, Red has actually made a coherent design decision! But of course it has the potential to cause massive confusion down the line once you start combining it with other operators.)

Errr... :face_with_diagonal_mouth: well, coherence for me would have commutativity for addition and multiplication. And if something needed to be non-commutative it could be another operator.

The not-likely-to-be-original idea I had was to order the "heart bytes" in the types.r table for numeric types based loosely on "complexity" (where integers are simpler than decimals, decimals are simpler than pairs or percents etc.). Then have some pre-dispatch code for ADD and MULTIPLY which will if necessary swap the arguments to put the more complex argument in the first position.

So roughly:

//
//  /multiply: native [
//
//  "Returns the multiplicative product of two values (commutative)"
//
//      return: [any-scalar?]
//      value1 [any-scalar?]
//      value2 [any-scalar?]
//  ]
//
DECLARE_NATIVE(multiply)
{
    INCLUDE_PARAMS_OF_MULTIPLY;  // for ARG(value1) vs. ARG(1), etc.

    Value* v1 = ARG(value1);
    Value* v2 = ARG(value2);

    if (HEART_BYTE(v1) < HEART_BYTE(v2)) {
        Move_Cell(SPARE, v2);
        Move_Cell(v2, v1);  // ...move simpler type to be on the right
        Move_Cell(v1, SPARE);
    }

    return Run_Generic_Dispatch(v1, SYM_MULTIPLY);
}

(I've rethought "generics" so they all basically work like this, allowing some common prelude/postscript code. Still just dispatch on one argument's type, but it can be any argument...or a synthesized value chosen based on the arguments. Multiple dispatch would be nice but have no idea how to graft that in.)

Hence integers would only be asked to multiply other integers, decimals would only be asked to multiply decimals or integers, etc. More policies could be enforced in the prelude code if that makes it easier as well.

According to ChatGPT, a similar approach for type ordering is used in "SymPy" for commutativity:

I'm not completely crazy about ordering the types based on small integers in a table, but I'm beginning to kind of see the boundaries of how far this project is going to be able to get... and extensible datatypes may just not be the research space it gets to. I'd like to show off the isotopes and other novel features and think there's a lot of material there, without breaking some new ground in generic programming.

Imagine you have a slider control, on some user interface, where the slider lets you go from 0% to 100%. But the slider itself has integer bounds and doesn't understand the percent type.

It seems reasonable to me in such a world where the "0" to "100" is the range of what you get, that it is meaningful to say "but I mean this as a percent".

The question of what TO means--as I've said--is historically fraught. But I've been wrestling with the idea that it's fundamentally reversible, with a principal usage of pushing things "out of band" in a dialect, e.g. where there was already a meaning for the type you were using.

INTEGER! is already taken in your dialect? Ok, to tag! 10 => <10> for whatever other integer-valued idea you have.

Want your number back? to integer! <10> guaranteed to give you back 10, because all TO operations (that succeed) are reversible.

Historical TO has done many things, sometimes along those lines, and sometimes not. Enforcing reversibility and the "triviality" so people could count on TO was the thesis of this post:

Embracing A "Useless" Definition of TO

Working with that idea makes me feel like to percent! 10 would be 10%... for example, applicable in these integer-slider-bar-to-a-percent types of scenarios.

Fair enough, this would be a valid usecase for it. (Though personally Iā€™d have no problem with explicitly writing as percent (output / 100), to make it clear that the output is treated as a fraction of 100.)

1 Like