Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new mathematical operators #2887

Merged
merged 10 commits into from
Jan 24, 2014

Conversation

epidemian
Copy link
Contributor

This is an attempt to (finally) include a new set of mathematical operators beyond what JavaScript provides.

Included on this PR are the operators that seemed the most consensually accepted by the community and that seemed the most practical and intuitive in terms of syntax to me:

  • the power operator **,
  • the floor division operator // and
  • the (correct) modulo operator %%.

Other operators might be useful, but i think it's important not to over do it and try to convert every function on Math into a new operator. So i deliberately left out more sophisticated operators like LiveScript's chainable maximum and minimum operators (<? and >?) or a unicode operator for square roots 😸

That being said, this is an open pull request, so if you think other operators deserve to make it in (or some of these doesn't deserve it), please comment.


Small caveat

The floor division operator token clashes with the current empty regex token: //. I think there's no way to have them both; even if we did some grotesque lexer + parser magic introducing some ambiguity in the lexer (say, a FLOOR_DIV_OR_EMPTY_REGEX token) and then trying to disambiguate it in the parser, there are ugly corner cases, like a //i, which is valid on the current master as a call to a with an empty regex and is also valid in this PR as a floor division between a and i.

I opted to remove the support for empty regexes in this PR (notice a missing regex test), but i don't know if this is an acceptable removal. I would expect that nobody would ever use an empty regex, as i see no point in doing so, but i've been very mistaken about guessing what other people don't do on their code in the past. So, if we decide to remove the empty regex token, could we make an intermediate minor release that marks that token as a syntax error and some time after that release these new set of operators?

@epidemian
Copy link
Contributor Author

Related stuff: #1971, #2026

@davidchambers
Copy link
Contributor

Nice work, @epidemian!

@vendethiel
Copy link
Collaborator

a //i, which is valid on the current master as a call to a with an empty regex and is also valid in this PR as a floor division between a and i

This case could be fixed by requiring it to be either unspaced or dually-spaced, couldn't it ?

@epidemian
Copy link
Contributor Author

This case could be fixed by requiring it to be either unspaced or dually-spaced, couldn't it ?

Hmm... yes, but not without other nuances i think. When other operators appear at the end of a line they imply a line continuation:

# Parsed as "foo(bar + baz);"
foo bar +
baz

But with the // token as a regex, this has a valid meaning right now:

# Parsed as "foo(bar(/(?:)/)); baz;"
foo bar //
baz

(it seems the // also confuses the GitHub syntax highlighter too 😺 )

We could also opt for another symbol for floor division; though i would prefer it to be the same symbol as in Python.

@bjmiller
Copy link

I think the "easy" way to solve this is just use a different operator. Yes, "//" is the most make-sensical, and it's already well-known from python, but it saves a lot of effort to just use something different. I'll throw "/_" ("div-floor") out there, although it's neither better nor worse than most other alternatives.

@shesek
Copy link

shesek commented Mar 28, 2013

@bjmiller since _ is a valid javascript identifier, a /_ b is already parsed as a / _(b)

@michaelficarra
Copy link
Collaborator

@shesek and a/_b is already parsed as a / _b.

@epidemian
Copy link
Contributor Author

Sooo... any other thoughts on this PR? Any other operator that would be useful?


What should we do with the // empty regex literal? If the // is preferred for the floor division operator we could release a new compiler error in the next version of CS to avoid changing the meaning of users' code unexpectedly. Something like:

$ bin/coffee -cs <<<'a // b'
[stdin]:1:3: error: // reserved for future use as floor division operator. Use /(?:)/ for an empty regex.
a // b
  ^

Then, after that release, we could finally add the new shiny // operator and hopefully avoid having broken many users' code.


Also, about the documentation, @jashkenas is good with the words, but maybe we can help providing some sample snippets for the new operators (bonus point for a coherent example that showcases all three of them 😸). I may fiddle with this later if i feel inspired =P

@bjmiller
Copy link

bjmiller commented Apr 8, 2013

Just double-checking: Is there a real use case for an empty regex? Like, why would anyone intentionally create one?

Anyway, it's still my preference to come up with an operator that requires less work to implement. The problem is, I don't really see any other combination of symbols on the keyboard that make any kind of mnemonic sense to me.

@epidemian
Copy link
Contributor Author

Just double-checking: Is there a real use case for an empty regex? Like, why would anyone intentionally create one?

I have no idea. The /(?:)/ is always there too, which is what a REPL will yield if you type new RegExp :)

Anyway, it's still my preference to come up with an operator that requires less work to implement.

Implementing // as an operator is very easy actually. The only change regarding the // regex is this line here (instead of substituting it with /(?:)/ it returns an empty match, thus allowing // to be an operator).


About the documentation for the new operators, maybe instead of examples we can have a simple table showing the JS equivalents:

CoffeeScript JavaScript equivalent
Power a ** b Math.pow(a, b)
Floor division a // b Math.floor(a / b)
True modulo a %% b (a % b + b) % b

Though, to be in tone with the rest of the documentation maybe some runnable examples would be better, i don't know. Some ideas:

# For the modulo operator, maybe a circular array index.
arrAt = (arr, i) -> arr[i %% arr.length]
notes = ['do', 're', 'mi', 'fa', 'sol', 'la', 'si']
alert "The note before do is #{arrAt notes, -1}"
alert "And 7 notes after do is #{arrAt notes, 7} again!"

# For the floor division, any division that should be rounded would be good IMO.
seconds = (new Date - start) // 1000
alert "You have been reading the docs for #{seconds} seconds."
# (`start` could be taken when the page is loaded)

# For the power operator, maybe a statistical formula like exponential growth.
population = 7000
rate = 0.05
time = 10
finalPopulation = population * (1 + rate) ** time
alert "Population after 10 years: #{Math.floor endPopulation}"

# Not sure if a good example for the power operator, but i like it =P
φ = 1.618033988749895
ψ = -0.6180339887498949
# A fast fiboncci number formula.
fastFib = (n) ->** n - ψ ** n) /- ψ)
alert "fib(8) = #{fastFib 8}"

I think i would prefer the table for succinctness' sake though.

(totally off-topic: why does the GH highlighter discriminate the awesome φ and ψ symbols with that ugly red background!?)

@michaelficarra
Copy link
Collaborator

why does the GH highlighter discriminate the awesome φ and ψ symbols with that ugly red background!?

Because pygments sucks. It has one job, and it can never get it right. At least it no longer thinks herecomments never end.

@erisdev
Copy link

erisdev commented Apr 10, 2013

@epidemian pygments believes non-ASCII characters are not valid in CoffeeScript (and presumably many other languages) source. a unicode travesty.

@vendethiel
Copy link
Collaborator

So long you don't turn Coffee into APL :p.

@lydell
Copy link
Collaborator

lydell commented Apr 15, 2013

Perhaps not so related to this PR, but when I think about maths and CoffeeScript, this is what comes up in my mind:

Maths and CoffeeScript teacher: “What is the result of sin π + 5?”
Student: “That depends on what you're currently teaching.” (Assuming {sin} = Math; π = Math.PI)

@michaelficarra
Copy link
Collaborator

@lydell: Agreed, the juxtaposition operator (implicit function application) should have higher precedence than all other infix operators. But that's a discussion for a different issue.

edit: Also, you can do renaming in destructuring assignment: {sin, PI: π} = Math.

@epidemian
Copy link
Contributor Author

Thanks for clarifying, @michaelficarra, i wasn't sure what @lydell was pointing out.


I'd appreciate if we could have some closure for this issue. Should i invoke @jashkenas? :)

Jeremy: do you think this PR is acceptable? What do you think we should do about the clash between the floor operator and the empty regex (//)?

@michaelficarra
Copy link
Collaborator

@epidemian: I believe we were all happy about the power operator and floored division, but the max/min (which I'm strongly for) and fixed modulo operators were still undecided.

@epidemian
Copy link
Contributor Author

@michaelficarra, thanks for the feedback.

For the min/max operators, i guess you're referring to the LiveScript's cute <? and >? (please correct me if i'm wrong).

For those who do not know, they are basically the operator form of Math.min and Math.max respectively. And they are chainable! So a <? b <? c is equivalent to Math.min a, b, c; though it probably should be compiled to the more hand-rolled (_ref = a < b ? a : b) < c ? _ref : c form, as they do on LiveScript, to allow for comparison of strings for example.

I personally find these two operators to be quite intuitive and consistent with current binary ? operator: a <? b can be read as "if a is less than b then a else b" in the same way that a ? b can be read as "if a is defined then a else b".

That being said, the other operators included in this PR were chosen mostly for their "familiarity" in other languages as well as their convenience. In the case of **, it was chosen mostly for the familiarity factor, as using Math.pow instead is not that big of a deal, but programmers coming from Python or Ruby (both of which could be considered Coffee's ancestors) as well as other languages might expect a power operator instead. // was chosen mostly for it's convenience, as floored division is kind of the norm when dealing with integer numbers. And %% was chosen for a mixture of both familiarity and convenience: lots of other languages implement modulo as the proposed %% instead of JS's %, and being able to write a %% b instead of remembering the (a % b + b) % b formula is pretty convenient too.

In the case of <? and >?, i don't think they are so decisively convenient over using Math.max/min, and i feel they are not as popular in other programming languages either. Besides, we should always consider the extra complexity of adding new operators, which is mostly adding one more case to the precedence rules.

All in all, my vote for the inclusion of these two operators is neutral. The popularity of the operators in other languages doesn't really bother me that much (chained comparisons are not that popular outside Coffee either, Python being the only exception that comes to mind, and they're damn useful for example). I'll not be sad if they are rejected, but i will happily implement them if they are accepted 😺

I'm interested in what other people think.

@vendethiel
Copy link
Collaborator

I don't think I've used coco's More than 4 times in a year, so I don't dind them that attractive

@satyr
Copy link
Collaborator

satyr commented Apr 17, 2013

to allow for comparison of strings for example

They're mostly for strings to begin with, as you should be using Math.{min,max} for numbers. I don't consider them mathematical operators.

@jashkenas
Copy link
Owner

@xixixao I think probably go for it. My two questions are:

Does the modulo operator really need a helper function, instead of being compiled inline?

What are some good real-world use cases for the modulo operator, where the existing remainder operator % won't suffice?

@xixixao
Copy link
Contributor

xixixao commented Jan 24, 2014

Many cases, I have written the following pattern way too many times in my life:

i = something
array[(i + array.length) % array.length]

(notice that's not even bulletproof). +1 for inline compilation, @epidemian ?

@jashkenas
Copy link
Owner

I have written the following pattern way too many times in my life

That's exactly what I mean — you're just using the remainder operator there, not @epidemian's proposed "correct" modulo operator. Where does the correct one work that the remainder won't help you?

@xixixao
Copy link
Contributor

xixixao commented Jan 24, 2014

@jashkenas My code is error prone and longer than

i = something
array[i %% array.length]

Now the correct modulo not only fixes the case of negative left operand but also negative right operand.

@jashkenas
Copy link
Owner

Why would you have a negative RHS?

@xixixao
Copy link
Contributor

xixixao commented Jan 24, 2014

Why not? I mean that is not applicable to my example, but it is likely that some algorithm might depend on modulo with negative divisor. If not, we can compile to the more relaxed version.

@xixixao
Copy link
Contributor

xixixao commented Jan 24, 2014

Original discussion on modulo starts with your comment here: #1971 (comment)

@jashkenas
Copy link
Owner

it is likely that some algorithm might depend on modulo with negative divisor

That's exactly what I'm asking. Have any of you ever needed "true" modulo for real-world programming before? If so, what for? If not — we can just stick with %, and leave things simple, no?

@epidemian
Copy link
Contributor Author

@jashkenas,

Does the modulo operator really need a helper function, instead of being compiled inline?

Nope, it's just for convenience/laziness: a separate function makes it very easy to guarantee that each operator is evaluated only once. If we were to compile this in-line, we should assign the value the second operand (if it isn't a simple expressions) to an intermediate variable, because it's used more than once in the operation.

A bit more complex, but yes, there should be no problem in doing so. I personally prefer having a separate helper function, as i think _modulo(42, foo()) is much more obvious in its meaning than (42 % (_ref = foo()) + _ref) % _ref. If someone wants to take a stab at compiling it inline, go for it; i may take a look at it tonight.

What are some good real-world use cases for the modulo operator, where the existing remainder operator % won't suffice?

Anything that "wraps around" and may be negative. For example, a "neighbours" function in a Game of Life (stolen from the first article i found...):

neighbours = ([x, y]) ->
  x0 = (x - 1) %% N
  x1 = x
  x2 = (x + 1) %% N
  y0 = (y - 1) %% N
  y1 = y
  y2 = (y + 1) %% N
  [[x1, y0], [x2, y0], [x2, y1], [x2, y2], [x1, y2], [x0, y2], [x0, y1], [x0, y0]]

These kind of operations are pretty common in games :)

If you would prefer to keep this operator out, i'd totally understand. I'm not sure either if having a correct modulo operation is worth the complexity of having a new operator.

@jashkenas
Copy link
Owner

@epidemian Great example, let's leave it in then.

And your argument about having it as a helper is persuasive as well. Sounds just fine. Feel free to fix up this PR so that it applies cleanly, and then merge away.

@satyr
Copy link
Collaborator

satyr commented Jan 24, 2014

Make sure to force numbers in the modulo helper. You'd get nonsenses like:

'1' %% '42' == 16

@L8D
Copy link

L8D commented Jan 24, 2014

@satyr There is no way to effectively type check what a variable would be other than in their literal form. If you want to prevent them from being used in literal form, I really don't think there will ever be a case where someone would need/want that.

@xixixao
Copy link
Contributor

xixixao commented Jan 24, 2014

@satyr '1' % '42' gives 1 in CS, this is up to the user.

@lydell
Copy link
Collaborator

lydell commented Jan 24, 2014

I think it’s worth fixing @satyr’s issue. We only need to add two +:

 function(a, b) { return (a % +b + +b) % b; }

That makes %% behave like all other mathematical operators: Using numbery strings as operands gives the same result as using numbers.

@xixixao therefore '1' %% '42' should give 1 as well.

@xixixao
Copy link
Contributor

xixixao commented Jan 24, 2014

You only need the second + I believe. Otherwise it makes sense. But I disagree with

all other mathematical operators: Using numbery strings as operands gives the same result as using numbers.

coffee> '1' + '42'
'142'

The lesson being you should really not use strings instead of numbers.

Conflicts:
	lib/coffee-script/grammar.js
	lib/coffee-script/lexer.js
	lib/coffee-script/nodes.js
	lib/coffee-script/parser.js
	test/regexps.coffee
@epidemian
Copy link
Contributor Author

PR should be mergeable now :)

Should i add @lydell's trick to convert the arguments into numbers (using @xixixao's suggestion)?

function(a, b) { return (a % b + +b) % b; }

@michaelficarra
Copy link
Collaborator

@epidemian: Yes, please.

@epidemian
Copy link
Contributor Author

Done.

(555 tests!)

check = (a, b, expected) ->
res = a %% b
# Don't use eq because it treats 0 as different to -0.
ok res == expected or isNaN(res) and isNaN(expected),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use == either. Why can't we distinguish 0 from -0 in the tests?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

== is the same as is, let's go with the latter :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michaelficarra, yeah, the only curious case is 0 %% -1, which evaluates to -0 (which makes sense since the result of the modulus should have the same sign as the divisor), so we could assert that in the tests and be explicit 👍

@lydell
Copy link
Collaborator

lydell commented Jan 24, 2014

@xixixao oops, all other mathematical operators except + (which is the root of that issue). You’re right, + is the only exception.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.