problem-solving
problem-solving copied to clipboard
"Tight or" precedence level of //, min, max operators
"Tight or" has lower precedence than the "chaining infix" operators. This makes sense in the case of operators like || and ^^ that actually are logic operators in the first place - much less so for //, min and max that are just similar to the technicalities of ||.
Hello, after the short summary, I would like to elaborate on my point more... First off, I would like to point out the conceptional inconsistency in the docs: the "chaining infix" operators share a more important thing than the technicalities of chaining - they describe relations and hence they return boolean values. The name "tight or", on the other hand, tries to capture this property of the operators in the group, namely that they appear in logical expressions where they have the properties of a logical "or". The thing is, 3 relations out of 5 in that group aren't really or-like, moreover it doesn't make much sense to use them in logical expressions. It caught me while using R// but it doesn't take that much trickery to see this behavior is unintuitive, take for example: 3 < 1 min 4 # False - so far so good, right? 3 < 5 min 1 # 1 - WAT? 3 < 5 min 2 # True - umm... "would I lie to you, baby, would I lie to you" I don't think anyone would ever want this behavior as the "default". It reminds me of Pascal with "and" and "or" being higher precedence than comparing operators themselves - but Pascal came with the excuse that there were like 4 precedence levels altogether and on some level it was elegant to draw the parallel between * and "and", + and "or". In Raku, looking at that table, there might be around 20 precedence levels so I don't think this can serve as a basis to slapping predominantly logic-operators to predominantly value-operators. I personally think their reasonable place would be around the junction operators and the additive operators; anyways, this isn't the time and place to propose a new definition. At the moment, I just want to get confirmation that 1. this behavior is indeed unnecessarily unintuitive and generally useless (counter-arguments if there are) and 2. whether there are technical constraints behind the scenes that force the devs and the users into this compromise. Any feedback is welcome. :) M. Polgár
I don't know that I agree that "tight or" is intended to indicate that all operators in the group are to be "or-like". The level names in the original incarnation were primarily just shorthand for referring to operators at that level, they weren't intended to be descriptive or restrictive in the actual operators that could appear at that level.
What's really being captured at this precedence level is that the "min" and "max" operators have list associativity, as opposed to right or left associativity. And that list associativity is likely part of the reason why they're lower precedence, and should be kept away from the junctive or additive operators. Also, I think "min" and "max" belong in the same general vicinity as "cmp", upon which they are based. Finally, as list associative, they are perhaps a bit more like "list infix" operators that appear even farther down the list.
In terms of "3 < 5 min 1" giving 1, that feels like a valid result to me. "3 < 5" gives True, and "True min 1" can be either "True" or "1" since the two operands compare the same. (Try "True cmp 1" ==> # Same .)
So, "3 < 1 min 4" gives False because "False cmp 4" is Less, "3 < 5 min 1" gives 1 because "True cmp 1" is Same, and "3 < 5 min 2" gives True because "True cmp 2" gives Less. (False numerifies to 0 and True numerifies to 1 for these comparisons.)
Pm
Hello, thanks for the quick feedback. To be honest, I can't say I feel there were convincing points made.
In terms of "3 < 5 min 1" giving 1, that feels like a valid result to me. (...)
Here you are describing how the expression is evaluated. I know that, that's the cornerstone of the whole issue. The behavior is valid according to the specs - my argument is that this behavior is horrible to look at and nobody would really want it.
I don't know that I agree that "tight or" is intended to indicate that all operators in the group are to be "or-like". The level names in the original incarnation were primarily just shorthand for referring to operators at that level, they weren't intended to be descriptive or restrictive in the actual operators that could appear at that level.
This is a doc issue if anything; I think the names should be descriptive if they exist in the first place. They should give us some hint why actual logic operators like || and ^^ should have the same precedence as // or max. If the reason for that is the list associativity, the name should capture at least that.
Good that you mentioned "cmp" a couple of times - "cmp" actually has higher precedence than the "chaining infix operators. I for one would be pleased if these operators happened to have the same precedence as "cmp". :)
What's really being captured at this precedence level is that the "min" and "max" operators have list associativity, as opposed to right or left associativity. And that list associativity is likely part of the reason why they're lower precedence, and should be kept away from the junctive or additive operators.
I would focus on this part as this contains the most tangible evidence.
- First, I don't know if what you said applies to // as well - I think // would make zero sense to be list associative.
- Actually, I see no particularly good reason for min and max to be list associative, either... the described operations themselves are virtually associative, similarly to * and +. They could have any associativity and have the same output. Therefore, unlike Z, it's a mere technicality that they have list associativity.
- To elaborate on this point further: I would have thought an advantage of list-associativity could be in this case to give more predictable results regardless the order of operands. I was wrong. min isn't even commutative:
False min 0and0 min Falsearen't the same. However,0 min 2 min False # 0also doesn't have the same result as0 min False # False. This also feels like unnecessarily unintuitive behavior to me and it could instantly be fixed by switching to either left- or right-associativity; that would still be easier to comprehend.
Finally, as list associative, they are perhaps a bit more like "list infix" operators that appear even farther down the list.
Honestly, now that you mention it, "list infix" operators also have a precedence that's rarely ever useful! X and Z usually cry for parentheses. It probably all boils down to the very low precedence of the comma operator - an operator that not even the .raku output uses without the parens... So to me this seems like justifying a bad decision with another (more fundamental) bad decision. Let's say we accepted that anything involving lists will have useless default precedence and we better use parens - why should it follow that we need to extend that to //, min and max as well?
And anyway, touching the comma operator or the "list infix" operators would surely break everything around, that issue is far out of reach. Let's just focus on the mentioned 3 operators. Does list associativity really affect their precedence? Is it even worth having? Are there other, either technical or usage reasons?
The feedbacks are obviously still more than welcome. :)
I've been mulling this over since you opened the issue – I think you raise a lot of good points. Despite my mulling, I'm not sure I really reached a coherent conclusion, but I'll post my thoughts here in the hope that they're helpful – apologies in advance if this isn't as organized as I usually aim to be.
1. These sorts of precedence calls are especially subjective
By choosing to have operator precedence at all (unlike, say, a Lisp or an APL), Raku is already wading into pretty subjective waters. It's all going to turn on user expectations – which will, in turn, depend on what other programming languages/math systems that user has experience with. This is doubly true for operators like min or //, which aren't present in most programming languages, because there's less likely to be a "consensus" view of the correct precedence. (Many languages have min functions, but I can't think of one with a min operator.)
That said, I personally agree with you that 3 < 5 min 2 returning True is a bit of a WAT. I notice, however, that I'd actually expect 5 min 2 < 3 return True, so my personal (subjective) intuitive expectation seems to be that min and < are on the same precedence level and are left-associative. I wonder if you share that intuitive expectation? (This expectation can't actually be true, because < has chaining associativity and thus can't also be left-associative.)
I'm not as sure if I share your intuitions for // and R//. I definitely recall needing to use parenthesis with R// a few times when I thought I really shouldn't need to – but I can't come up with specific examples. I'd be interested to hear the context that first brought this to your attention.
2. min and max are a bit odd
Unlike most operators, min and max share their name with a Sub. As a result, the following is code is possible:
say 3 < 5 min 2 # OUTPUT «True» (your example)
say 3 < 5 [&min] 2 # OUTPUT: «False»
say 3 < min 5, 2 # OUTPUT: «False»
The second line is interesting, but not something I'd use/expect to see. But the third is pretty practical, and one I would expect to see. IMO, this removes a fair bit of the urgency from this issue, since there's already a way to express the minimum of 5 and 2 without needing to parenthesize. That doesn't remove the WAT, but makes it less of a big deal.
3. List associativity still matters for min and max
You said that:
the described operations themselves are virtually associative, similarly to
*and+. They could have any associativity and have the same output.
I agree, but that "virtually" can matter quite a bit – at least in the edge case of multiple ops with the same precedence level. Consider the expression 5 min 2 max 10: with left associativity, we'd get 10; with right, 5; with list (what we actually have) we get Only identical operators may be list associative; since 'min' and 'max' differ, they are non-associative and you need to clarify with parentheses.
(I'm actually not sure whether this means that min and max are not mathematically associative. Mathematical associativity is always talked about in the context of a single operator – how does it treat operators with tied precedence?)
You also said:
I would have thought an advantage of list-associativity could be in this case to give more predictable results regardless the order of operands. I was wrong. min isn't even commutative:
False min 0and0 min Falsearen't the same. However,0 min 2 min False # 0also doesn't have the same result as0 min False # False.
I agree that min isn't commutative and that list associativity should give more predictable results. However, list associativity should mean that 0 min False is the same as min(0, False) – but your last example shows that it isn't. I'm pretty sure that's just a Rakudo bug and I'll try to submit a PR.
4. Regarding docs issues
I generally agree with you that "the names [used in the docs] should be descriptive if they exist in the first place" and that the docs could be a bit clearer on that front. This issue inspired me to put together a docs PR (Raku/doc#4031) that took a first step along those lines (and also got sidetracked into some points that caused me to discover a few related Rakudo issues (rakudo/rakudo#4779 and rakudo/rakudo#4780)). But I'm sure there's more we could do to clarify this area.
I was kind of hoping I'd work my way to a conclusion, but no such luck. I'm not sure where I come down on the overall issue, but it is a good one to think about (and has lead to discovering/starting to fix some bugs, so it's already been productive!).
Hello, even if there is no conclusion, I'm happy that "we are working"; I think that thinking about these things is always beneficial some ways. Also... I think I have seen you under another issue I opened but only now do I recognize you as the author of the "105 C++ Algorithms in 1 line of Raku" video which I keep going back to... so thank you for that. :) It's my honor that pioneers of this community join these discussions.
I notice, however, that I'd actually expect 5 min 2 < 3 return True, so my personal (subjective) intuitive expectation seems to be that min and < are on the same precedence level and are left-associative.
I think you got the conclusion "backwards" here because the same result would follow from "min" having straight-up higher precedence than "<" (which my intuition would assume).
I'm not as sure if I share your intuitions for // and R//. I definitely recall needing to use parenthesis with R// a few times when I thought I really shouldn't need to – but I can't come up with specific examples. I'd be interested to hear the context that first brought this to your attention.
I wanted a one-liner without parentheses and eventually this not particularly useful precedence of R// ruined it... +@common-children <= (-1 R// @prev-filter.first: *.so, :end, :k) so yes, if the @prev-filter contains a True value that couldn't be zipped to @common-children, that will need a special treatment.
IMO, this removes a fair bit of the urgency from this issue, since there's already a way to express the minimum of 5 and 2 without needing to parenthesize. That doesn't remove the WAT, but makes it less of a big deal.
Indeed; I was mostly caught by // and then I looked it up and "extended" it. In that group, || and ^^ are "logical operators" and legitimately share the precedence, the rest I don't think so. I used examples with min because they are easier to construct and I think the point still stands for them.
Consider the expression 5 min 2 max 10: with left associativity, we'd get 10; with right, 5; with list (what we actually have) we get Only identical operators may be list associative; since 'min' and 'max' differ, they are non-associative and you need to clarify with parentheses.
I haven't thought about that, fair point. Although as we can see the advantage of list associativity is not some reasonable output but catching it as something unparseable. I think I would prefer this behavior for this expression regardless associativity but this seems like a technicality that shouldn't be tied with associativity in the strictest sense. Also, I still think that even for this expression both right and left associativity would result in an acceptable (although less desirable) output, not straight-up a WAT.
However, list associativity should mean that 0 min False is the same as min(0, False) – but your last example shows that it isn't. I'm pretty sure that's just a Rakudo bug and I'll try to submit a PR.
Oh that's good to know... this is how sidenotes can turn into useful evidence. :D One more reason why these discussions are useful.
Similarly, I'm happy to see that the issue kind of lead to doc improvements, although it feels a bit weird to create work for others this way... although I don't feel competent enough to make changes like this on my own. Some day, hopefully. :)
Also not a conclusion here but... I'm still bugged by the remark about list infix operators and the whole list business... having list construction as a basic operator rather than special syntax seems very elegant but I wonder why list-related operators must have such low precedences. Actually, I often feel positionals were always meant to be a parallel universe within Raku for some reason. List assignment as a separate thing, all the peculiarities of the % and @ sigils (including the weird destructuring I opened an issue for), hard to get it working with types... It would be good to know about decisions of this kind, whether it's operator precedence, or the special assignment type, or the difficulty of sub fun(Int @nums) {} because for me it still often feels like these things work differently from all languages I know just for the sake of being different... and then it escalates to other decisions in order to be consistent and we just can't see why it has to be like that in the first place.
only now do I recognize you as the author of the "105 C++ Algorithms in 1 line of Raku" video
I'm glad you liked the video and thanks for the kind words :) But I'm hardly a "pioneer", just someone who discovered Raku a few years ago and dove in (with a lot of help). The other participant in this thread, on the other hand, really is a pioneer in the Raku community: I'm not exactly sure when @pmichaud first got involved with Raku (then Perl 6), but it was at least ~15 years before I did. And that was the time that the true pioneering work was being done – the work that really laid the foundation for Raku as it exists today.
Indeed; I was mostly caught by
//and then I looked it up and "extended" it. In that group,||and^^are "logical operators" and legitimately share the precedence, the rest I don't think so.
Hmm, I'm not sure I agree. I definitely view // as a logical operator and, though I haven't really thought of min and max as logical operators, I'm starting to think that they might be (at least in infix form). Consider the following code (which I'm imagining as code for reviewing something on a scale from 0 stars to 5 stars):
$rating || 3;
$rating // 3;
$rating max 0;
$rating min 5;
In each expression, we check whether the LHS passes a predicate; if it does, we return the LHS; if not, we return the RHS. That is, we could achieve the same thing with:
$rating.so ?? $rating !! 3;
$rating.defined ?? $rating !! 3;
$rating > 0 ?? $rating !! 0;
$rating < 5 ?? $rating !! 5;
They're not logical operators in the strictest sense of the term because they don't return a boolean value. But && and || don't either, and I have no hesitation in calling them logical ops.
(One distinction we could draw is whether they short-circuit: || and // both do, but min and max don't).
[Edit: I'm also thinking about the points you raised in your "not a conclusion here but..." paragraph, and will comment separately after a bit more thought.]
"deleted" (because this comment was unhelpful)
Hi @raiph , I hope you to engage with some of my points because this doesn't seem particularly useful or related to anything. I said that I wanted to get that line right without needing parentheses - which failed on a not particularly useful or meaningful-looking precedence rule on R//. Not like it should matter at all as it is really just trivia.
"deleted" (because this comment was unkind)
Sorry 2colours.
I don't think you should act as if it was impossible to add something meaningful to the discussion - and if your best idea was to nitpick about my example use-case that bears little value to the overall topic I tried to set up, maybe you have something to wonder about with your attitude here in the first place.
I'm sorry to see that this conversation took a turn and am locking it for now to give everyone a chance to take a breath. @2colours, you've had some very interesting thoughts and seem like you'd add a lot to our community. That said, we try to be a pretty friendly group and your criticism of a good-faith comment wasn't in keeping with that. I can see why @raiph's first response was "ouch".
@raiph, you we one of the people who welcomed me into the Raku community. If I'd been greeted with comments like the rest of your comment, I'm not sure I would have felt all that welcome or gotten involved the way I did.
Both of you seem like good folks who got a bit frustrated, so lets all take a breath.
Well, I actually wanted to also react to @codesections 's message that I only saw afterwards but that opportunity came much later apparently... By the way, certain things don't depend on the number of "breaths taken"; like, if we are trying to put effort into a discussion, it's not very elegant at best to interrupt with a Stackoverflow-style comment mostly about the stylistics of an insignificant detail. :\ So I'm not so sure how to feel about self-proclaimed friendliness and good-faith.
I'm not exactly sure when @pmichaud first got involved with Raku (then Perl 6), but it was at least ~15 years before I did. And that was the time that the true pioneering work was being done – the work that really laid the foundation for Raku as it exists today.
Fair enough - got to give credit to those who did the main work without any big scene around it. (Also, I don't even know who I would recognize on a forum from those people I know about in the first place so it's not a very good measure. :) ) But at the end of the day, it's always like this: the appeals are more "attractive" in the literal sense, and I definitely wouldn't overlook the importance of "PR activity" even for programming.
In each expression, we check whether the LHS passes a predicate; if it does, we return the LHS; if not, we return the RHS.
I know what you mean and I kind of tried to imply it in the issue opener: "much less so for //, min and max that are just similar to the technicalities of ||." I think this is a "term mismatch" we are dealing with.
When I say "logical operator", I mean it in a near-strictest sense: it acts upon a logical value, enforcing boolean context. The attribute I quoted from you, I would rather call it "selector operator", for example. Given these quasi-definitions, I would say that ^^ and || as logical operators are based on abstractions that makes them selector operators as well (the semantics of ^^ is also interesting by the way; one-way exclusivity rather than a binary xor relation).
While // and possibly even min and max are also based on this abstraction, they have no particular use for boolean values - this is especially true for //.
I'm pretty sure $a == $b // $default will never do anything useful - actually I can't see it explicitly stated that $a == $b always returns True or False unless there is a Failure but I think this is implied; in this case, this will always evaluate to the True or False value of the equality.
I think, as a principle, the precedence of an operator should strongly take reasonable usage into account. You are right that there are many subjective questions here but this example with // wasn't really subjective: if you want to set a default value on the right side of a comparison (==, eq, < and the likes), you will need the parens, otherwise you end up with the result of the comparison, no matter the right handside of //. I think this (and the enforced context of an operator in general) is much more important to the user when creating an expression than underlying abstractions like what I called "selector operator".