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

Mod/intdiv Mathematical Operators to Work Around JavaScript Oddities #1971

Closed
mitsuhiko opened this issue Dec 25, 2011 · 22 comments
Closed

Mod/intdiv Mathematical Operators to Work Around JavaScript Oddities #1971

mitsuhiko opened this issue Dec 25, 2011 · 22 comments

Comments

@mitsuhiko
Copy link

One of the weird aspects of JavaScript is how it defines modulo:

> (-3) % 4
-3

In comparison Python/Ruby define them to always have the sign of the right side:

>>> -3 % 4
1

Likewise it's very common to have to divide and require an integer result. As such I would propose two new operators '%%' for right handside signed mod and '//' for integer division:

a %% b

would compile to

(a % b + b) % b

And

a // b

would compile to

Math.floor(a / b)

When is this useful? For instance consider wanting to move a player on a grid with the intent of mirroring over on edges:

goRight: ->
  @x = (@x + 1) %% @boardWidth

goLeft: ->
  @x = (@x - 1) %% @boardWidth

Another use case (where also the integer division comes in handy) is converting for instance things like global coordinates to locals:

getTile: (x, y) ->
   sectionX = x // SECTION_WIDTH
   sectionY = y // SECTION_HEIGHT
   localX = x %% SECTION_WIDTH
   localY = y %% SECTION_HEIGHT
   section = @sections[sectionX][sectionY]
   section[localX + localY * SECTION_WIDTH]
@showell
Copy link

showell commented Dec 27, 2011

+1 for //, although I believe it's been proposed before. I like it in Python. Is there a reason to prefer to parseInt to Math.floor?

+0.1 for %%. I've been bitten by JS's strange modulo behavior, but I think the workaround is idiomatic enough that it might not justify new syntax. I don't have any strong objections.

@mitsuhiko
Copy link
Author

Should totally be Math.floor.

@michaelficarra
Copy link
Collaborator

Sample compilation:

p %% r
s // t
var __modulo = function(a, b) { var c = a % b; return c < 0 ? c + b : c; },
    __floor = Math.floor;
__modulo(p, r);
__floor(s / t);

@mitsuhiko
Copy link
Author

The advantage that CoffeeScript's compilation support has is that we could avoid the function call by compiling the modulo to this:

a() %% b()

->

(a() % (_ref = b()) + _ref) % _ref

Advantages: branch free, no function call involved for the actual modulo.

@ghost
Copy link

ghost commented Dec 27, 2011

See also: Numeric#divmod in Ruby and divmod in Python.

@michaelficarra
Copy link
Collaborator

Yep, except that's not very readable. We could inline all of the helpers we have, but we don't because it wouldn't be as readable or as DRY.

@STRd6
Copy link
Contributor

STRd6 commented Dec 27, 2011

For a fixed modulo I often choose to extend Number.prototype:

Number::mod = (base) ->
  result = this % base

  if result < 0 && base > 0
    result += base

  return result

That way I can do

x = -3
x.mod 4

I do the same with Math.floor as well:

Number::floor = ->
  Math.floor(this)

(x / SECTION_WIDTH).floor()

New operators are a cool idea, but adding syntax has big hidden costs. I just want to illustrate another way to do it.

@mitsuhiko
Copy link
Author

Aside from the fact that patching prototype is literally the worst way to make this work, I understand that a helper function can be used as a replacement. That's in fact what I do currently.

Just pointing out that it's so very common a operation that deserves an operator (or two in this case).

@showell
Copy link

showell commented Dec 27, 2011

I searched some code that I wrote, and Math.floor shows up in the same line as division once every 500 lines or so, and a quick inspection suggests they're almost all candidates for '//'. It's often enough to be annoying, but maybe not worth new syntax. Searching for %% use cases is a little more difficult, since the negative number gotcha depends on the context, but a rough count shows about the same frequency.

@mitsuhiko
Copy link
Author

It's often enough to be annoying, but maybe not worth new syntax. Searching for %% use cases is a little more difficult, since the negative number gotcha depends on the context, but a rough count shows about the same frequency.

The problem is that many people work around the stupid mod operator in JavaScript by hand now instead of using the modulo operator. Wrapping around on left and right is a very common thing to do. I use modulo all the time in Python but I stopped using that in JavaScript and instead went to if conditions since the step size is most of the time 1.

The availability of such an operator would make people actually use mod more :-)

@STRd6
Copy link
Contributor

STRd6 commented Dec 27, 2011

Aside from the fact that patching prototype is literally the worst way to make this work

@mitsuhiko Why is patching Number.prototype the worst way? What's so bad about it?

@mitsuhiko
Copy link
Author

Impossible to properly isolate. Leads to possible namespace collisions. It's called monkeypatch for a reason :-)

Seriously though. It gives you nothing over a function and it comes with downsides, so why use it.

@showell
Copy link

showell commented Dec 28, 2011

See #80. Jeremy rejected // and ** pretty early on.

@dmmalam
Copy link

dmmalam commented Jan 7, 2012

+1 for %%

Been bitten too many times
https://github.com/dmmalam/Responsly.js/blob/master/slidy/slidy.js#L43

@jrus
Copy link

jrus commented Feb 12, 2012

The first implementation mentioned, i.e.

mod = (x, y) ->
    (x % y + y) % y

works properly in most cases, and is definitely the simplest implementation, but it falls down in a few edge cases. I use the following in my "mathutils", which is somewhat slower but should always be correct:

mod = (x, y) ->
    unless (jsmod = x % y) and (x > 0 ^ y > 0) then jsmod
    else jsmod + y

@aseemk
Copy link
Contributor

aseemk commented May 20, 2012

Would love this.

@michaelficarra
Copy link
Collaborator

@jashkenas: I think it's about time for you to put the hammer down on this issue. Is CoffeeScript going to get modulo/intdiv operators?

@jashkenas
Copy link
Owner

I think it's interesting, but should be rolled up as part of a comprehensive proposal to add the "complete" suite of new math operators -- including a power operator, and others we've discussed before.

@xixixao
Copy link
Contributor

xixixao commented Sep 9, 2012

I came up with this implementation for the new modulo operator, lengthy but seems fastest:

if (_res= a % b) < 0 && b> 0 || _res > 0 && b < 0 then _res + b
else _res

(more here)

Since noone mentioned it yet, I would like to point out that integer division, at least for me, is a composition of two operations: division and rounding. Even though the way it works is well established, because it is the first kind of division we learn in school, languages like Python ran into some trouble with including integer divison and adding new operator doesn't get rid of the ambiguity for newcomers (oh my, two divisions, which one should I use, let's hack). Not having to deal with integer division, learning how to get the correct value I want (rounding up/down/fairly) properly has certain value to it. Sure, it is a lot of typing but it is much more visible what I am doing when using .floor.

Back to modulo, I actually wish this was the default % behaviour and I would never have to do again
array[(index + array.length) % array.length]

@epidemian
Copy link
Contributor

I don't know why, but sometimes i like falling in these seemingly superfluous performance comparisons. Thanks for the blog post @xixixao! =D

I borrowed the mod implementations you posted on your blog and made this "correctness" test:

runTests = (modFn, name) ->
  t = (a, b, expected) ->
    res = modFn a, b
    if res isnt expected and not ((isNaN res) and (isNaN expected))
      console.log "#{name} - error on #{a} %% #{b}. Expected #{expected}, got #{res}"
  t 0, 1, 0
  t 0, -1, 0
  t 1, 0, NaN
  t 1, 2, 1
  t 1, -2, -1
  t 1, 3, 1
  t 2, 3, 2
  t 3, 3, 0
  t 4, 3, 1
  t -1, 3, 2
  t -2, 3, 1
  t -3, 3, 0
  t -4, 3, 2
  t 5.5, 2.5, 0.5
  t -5.5, 2.5, 2.0

fastMod = (n, base) -> 
  if (_result = n % base) < 0 && base > 0 || _result > 0 && base < 0
    _result + base
  else _result

originalMod = (n, base) ->
  (n % base + base) % base

saferMod = (n, base) ->
  unless (jsmod = n % base) and (n > 0 ^ base > 0) then jsmod
  else jsmod + base

fasterSaferMod = (n, base) ->
  unless (jsmod = n % base) and ((n > 0) != (base > 0)) then jsmod
  else jsmod + base

runTests fastMod, 'Fast mod'
runTests originalMod, 'Original mod'
runTests saferMod, 'Safer mod'
runTests fasterSaferMod, 'Faster safer mod'

All implementations seem to be equivalent and correct. Please let me know if i forgot some use-case.

Also, the performance seems to depend a lot on which browser is used and whether the inputs are natural numbers, integers o floats. Take a look at this JS Perf. I've only tested it on Firefox and Chrome under Ubuntu. For integers and floats, the "fast" and "faster safer" implementations seems to perform better, but for natural numbers the "original" implementation seems to beat everyone on Firefox (i was kinda (positively) surprised to see Firefox out-perform Chrome this much TBH).

Given that the performance seems to be so browser/data dependant, i'd suggest to go with the simplest and clearer implementation: the original (a % b + b) % b for a %% b. Browser implementations can change at any time; clear code lasts forever =P

@xixixao
Copy link
Contributor

xixixao commented Sep 9, 2012

Thanks @epidemian, I didn't know JS Perf and I am glad it matches my results (uff). I agree with the outcome, especially because of the look of the generated Javascript.

@vendethiel
Copy link
Collaborator

I think it's interesting, but should be rolled up as part of a comprehensive proposal to add the "complete" suite of new math operators -- including a power operator, and others we've discussed before.

Moving to #2887

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

No branches or pull requests