10

Grouping operator ( ) in JavaScript

The grouping operator ( ) controls the precedence of evaluation in expressions.


Does the functionality ( ) in JavaScript itself differ from Haskell or any other programming languages?

In other words,

Is the functionality ( ) in programming languages itself affected by evaluation strategies ?

Perhaps we can share the code below:

a() * (b() + c())

to discuss the topic here, but not limited to the example.

Please feel free to use your own examples to illustrate. Thanks.


smiknof
  • 129
  • 5
  • 3
    For language like Common Lisp and Clojure, `()`s have a very specific meaning. They're used to group in a way, but that's kind of a byproduct of their main purpose. – Carcigenicate Sep 30 '21 at 00:14
  • @Carcigenicate Assuming such an exception as a byproduct of their main purpose in Lisp/Clojure, what do you think about JavaScript and Haskell especially?? – smiknof Sep 30 '21 at 00:37
  • 4
    The question is broken. Evaluation order says, "given this expression, in which order should I evaluate its subterms?". Parentheses for grouping *change the expression*, that is, the collection of available subterms, not the *order* that subterms are visited in. – Daniel Wagner Sep 30 '21 at 00:43
  • 1
    In many languages (including JavaScript), parenthesis are used to call functions. In a few languages (including Haskell), parenthesis are solely used for grouping. – Bergi Sep 30 '21 at 02:48
  • 3
    Personally I wouldn't call grouping an "operator". I think of operators as things like `+`, `*`, etc that you apply to other terms. I *guess* you can call it a unary operator which returns its argument unchanged, using it for its "syntactic side effects" much like Haskellers use `$`. And indeed if you didn't have grouping parentheses but had some other way of demarcating the extent of an operator's argument and had user-defined unary operators, you could make one solely to use for grouping. Still seems a weird way to explain it to me, but *shrug*. – Ben Sep 30 '21 at 04:43
  • @Bergi "In many languages (including JavaScript), parenthesis are used to call function" The question clearly specifies "the functionality of Grouping operator `()` in JavaScript", so general usage of () is off-topic here. – SmoothTraderKen Sep 30 '21 at 21:03
  • @SmoothTraderKen However, the two syntaxes interact with each other, and we cannot answer a question about the precedence of `( )` without looking at both. The code example given uses three calls and only one grouping operator - `a * (b + c)` would've been simpler. And given the question is also about Haskell, better examples might be `a (b)` or `a (b, c)` which show the ambiguity of `( )` - they're also valid JS but mean something different. – Bergi Sep 30 '21 at 22:14
  • @Bergi " the two syntaxes interact with each other, and we cannot answer a question about the precedence of ( ) without looking at both." True, and that is what the question asks, and in fact, Ben clarified that using another `{ }` in his answer below. Thanks. – SmoothTraderKen Sep 30 '21 at 22:17
  • @Bergi I assumed the point of using `a() * (b() + c())` rather than `a * (b + c)` was so that each operand would actually be something that needed evaluation, since the OP's question was based around what effect the grouping would have on the order of evaluation in the context of a lazy language (even though in Haskell you don't really have nullary functions, so there's no direct equivalent of `a()`). – Ben Sep 30 '21 at 23:14

3 Answers3

10

Grouping parentheses mean the same thing in Haskell as they do in high school mathematics. They group a sub-expression into a single term. This is also what they mean in Javascript and most other programming language1, so you don't have to relearn this for Haskell coming from other common languages, if you have learnt it the right way.

Unfortunately, this grouping is often explained as meaning "the expression inside the parentheses must be evaluated before the outside". This comes from the order of steps you would follow to evaluate the expression in a strict language (like high school mathematics). However the grouping really isn't really about the order in which you evaluate things, even in that setting. Instead it is used to determine what the expression actually is at all, which you need to know before you can do anythign at all with the expression, let alone evaluate it. Grouping is generally resolved as part of parsing the language, totally separate from the order in which any runtime evaluation takes place.

Let's consider the OP's example, but I'm going to declare that function call syntax is f{} rather than f() just to avoid using the same symbol for two purposes. So in my newly-made-up syntax, the OP's example is:

a{} * (b{} + c{})

This means:

  • a is called on zero arguments
  • b is called on zero arguments
  • c is called on zero arguments
  • + is called on two arguments; the left argument is the result of b{}, and the right argument is the result of c{}
  • * is called on two arguments: the left argument is the result of a{}, and the right argument is the result of b{} + c{}

Note I have not numbered these. This is just an unordered list of sub-expressions that are present, not an order in which we must evaluate them.

If our example had not used grouping parentheses, it would be a{} * b{} + c{}, and our list of sub-expressions would instead be:

  • a is called on zero arguments
  • b is called on zero arguments
  • c is called on zero arguments
  • + is called on two arguments; the left argument is the result of a{} * b{}, and the right argument is the result of c{}
  • * is called on two arguments: the left argument is the result of a{}, and the right argument is the result of b{}

This is simply a different set of sub-expressions from the first (because the overall expression doesn't mean the same thing). That is all that grouping parentheses do; they allow you to specify which sub-expressions are being passed as arguments to other sub-expressions2.

Now, in a strict language "what is being passed to what" does matter quite a bit to evaluation order. It is impossible in a strict language to call anything on "the result of a{} + b{} without first having evaluated a{} + b{} (and we can't call + without evaluating a{} and b{}). But even though the grouping determines what is being passed to what, and that partially determines evaluation order3, grouping isn't really "about" evaluation order. Evaluation order can change as a result of changing the grouping in our expression, but changing the grouping makes it a different expression, so almost anything can change as a result of changing grouping!

Non-strict languages like Haskell make it especially clear that grouping is not about order of evaluation, because in non-strict languages you can pass something like "the result of a{} + b{}" as an argument before you actually evaluate that result. So in my lists of subexpressions above, any order at all could potentially be possible. The grouping doesn't determine it at all.

A language needs other rules beyond just the grouping of sub-expressions to pin down evaluation order (if it wants to specify the order), whether it's strict or lazy. So since you need other rules to determine it anyway, it is best (in my opinion) to think of evaluation order as a totally separate concept than grouping. Mixing them up seems like a shortcut when you're learning high school mathematics, but it's just a handicap in more general settings.


1 In languages with roughly C-like syntax, parentheses are also used for calling functions, as in func(arg1, arg2, arg3). The OP themselves has assumed this syntax in their a() * (b() + c()) example, where this is presumably calling a, b, and c as functions (passing each of them zero arguments).

This usage is totally unrelated to grouping parentheses, and Haskell does not use parentheses for calling functions. But there can be some confusion because the necessity of using parentheses to call functions in C-like syntax sometimes avoids the need for grouping parentheses e.g. in func(2 + 3) * 6 it is unambiguous that 2 + 3 is being passed to func and the result is being multiplied by 6; in Haskell syntax you would need some grouping parentheses because func 2 + 3 * 6 without parentheses is interpreted as the same thing as (func 2) + (3 * 6), which is not func (2 + 3) * 6.

C-like syntax is not alone in using parentheses for two totally unrelated purposes; Haskell overloads parentheses too, just for different things in addition to grouping. Haskell also uses them as part of the syntax for writing tuples (e.g. (1, True, 'c')), and the unit type/value () which you may or may not want to regard as just an "empty tuple".


2 Which is also what associativity and precedence rules for operators do. Without knowing that * is higher precedence than +, a * b + c is ambiguous; there would be no way to know what it means. With the precedence rules, we know that a * b + c means "add c to the result of multiplying a and b", but we now have no way to write down what we mean when we want "multiply a by the result of adding b and c" unless we also allow grouping parentheses.


3 Even in a strict language the grouping only partially determines evaluation order. If you look at my "lists of sub-expressions" above it's clear that in a strict language we need to have evaluated a{}, b{}, and c{} early on, but nothing determines whether we evaluate a{} first and then b{} and then c{}, or c{} first, and then a{} and then b{}, or any other permutation. We could even evaluate only the two of them in the innermost +/* application (in either order), and then the operator application before evaluating the third named function call, etc etc.

Even in a strict language, the need to evaluate arguments before the call they are passed to does not fully determine evaluation order from the grouping. Grouping just provides some constraints.


4 In general in a lazy language evaluation of a given call happens a bit at a time, as it is needed, so in fact in general all of the sub-evaluations in a given expression could be interleaved in a complicated fashion (not happening precisely one after the other) anyway.

Ben
  • 68,572
  • 20
  • 126
  • 174
  • 1
    If you consider statements in JS than the evaluation order needs to coincide with their lexical order of occurrence, because statements rely on side effects. For this reason I'm not sure whether in the presence of side effects the evaluation order of subexpressions is free to pick. Anyway, priceless answer! –  Sep 30 '21 at 07:56
  • 3
    @IvenMarquardt Specifying left-to-right order of evaluation makes a lot of sense in an impure language so the programmer can predict the order of side effects, but it still doesn't **need** to be that way. I believe C and C++ don't specify the evaluation order of arguments to the same call, for example (just that they'll all be evaluated before the call); the compiler is free to reorder for optimisation, and if the order matters the programmer is responsible for realising that and pulling the expressions out into separate assignment statements so the order can be controlled. – Ben Sep 30 '21 at 08:54
  • @Ben Thanks for your great answer, and please excuse me to have sent you my request to edit for readers to find the conclusion: "it is best (in my opinion) to think of evaluation order as a totally separate concept than grouping" more easily. – SmoothTraderKen Sep 30 '21 at 22:56
  • 1
    @SmoothTraderKen Thanks for the suggestion; I edited it a bit, since that paragraph was originally written as part of footnote 3 and didn't quite make sense in the new context, but I've taken your suggestion to promote that line to my main conclusion. – Ben Sep 30 '21 at 23:02
  • @Ben We would appreciate if you could also answer this question if you have time *Does the Hack-style pipe operator |> take precedence over grouping operator ( ) in order of operations?* https://stackoverflow.com/questions/69412553/ – SmoothTraderKen Oct 02 '21 at 00:00
2

Answer by myself (the Questioner), however, I am willing to be examined, and still waiting for your answer (not opinion based):

Grouping operator () in every language share the common functionality to compose Dependency graph.

In mathematics, computer science and digital electronics, a dependency graph is a directed graph representing dependencies of several objects towards each other. It is possible to derive an evaluation order or the absence of an evaluation order that respects the given dependencies from the dependency graph.

dependency graph 1 dependency graph 2

the functionality of Grouping operator () itself is not affected by evaluation strategies of any languages.

smiknof
  • 129
  • 5
2

To clarify the dependency graph:

enter image description here

KenSmooth
  • 275
  • 1
  • 10
  • 2
    That's how grouping works, yes, but I don't really see how this answers the question whether this is language-agnostic. – Bergi Oct 01 '21 at 22:49
  • To be precise, this is a supplement to the answer by @Ben. It will help to all readers. – KenSmooth Oct 01 '21 at 23:32