Charlie Harvey

Why is Math.max() less than Math.min()?

A while back I wrote a post called Javascript: the Weird parts primarily for my own amusement. It listed some of my personal favourite wtfjs-type Javascript idiosyncracies.

In this post I want to look in more depth at one of those idiosyncracies. I want to investigate exactly why Math.max() is less than Math.min(). Here’s what I mean, this is nodejs, but as we will discover, other implementations ought to behave the same.

> Math.max()>Math.min() false > Math.max()<Math.min() true

What should Math.max() do?

Let’s start out by clarifying that Math.max() is not the same thing as Number.MAX_VALUE. It doesn’t return the largest possible number or anything like that.

In fact it is for comparing a list of numbers and returning the largest. Here’s the definition from the ECMA-262 standard.

15.8.2.11 max ( [ value1 [ , value2 [ , … ] ] ] )

Given zero or more arguments, calls ToNumber on each of the arguments and returns the largest of the resulting values.

If no arguments are given, the result is −∞.
If any value is NaN, the result is NaN.
The comparison of values to determine the largest value is done as in 11.8.5 except that +0 is considered to be larger than −0.

Let’s quickly check that is what happens in the real world.

> Math.max(1,2,3) 3 > Math.max(3,2,1) 3 > Math.max(3) 3 > Math.max() -Infinity

Math.min is defined similarly, except that it finds the smallest number in its argument list. And if given an empty list it returns positive infinity. Let’s try it.

> Math.min(9) 9 > Math.min(9,1,7,2) 1 > Math.min() Infinity

Both functions work. But why is the largest number in an empty list negative infinity?

Folding with reduce

A nice way to look at this is through the lens of functional programming and abstract algebra. No, really. Bear with me.

Imagine that you wanted to implement max in a functional style. You want to reduce a list to the largest element in it. In the functional world, when we have a list that needs to be reduced to a single value it is typical to use a fold function — AKA reduce in Javascript and some other languages.

Reduce is an higher-order function. You pass it a function that works on two arguments, a function and a special initial value — the sense in which I mean special will become evident shortly. It applies the function to each element of the array, eventually reducing the contents of the entire array into a single value.

Say I have a list and I want to add up all the numbers in it. list = [1,2,3]

I can write a function that adds two numbers given as arguments, and then reduce the list to a single value using this function.

Here is my function. I’ll call it sum.sum = function(special,n) { return special + n } And I can call reduce with it and the special value 0 on a list and have it do as expected. > sum = function(special,n) { return special + n } [Function] > special = 0 0 > list.reduce(sum, special) 6

For each element of the list we call sum with the special value, which is initialized to zero, and behaves like a running total.

Note how this is pretty much the same as taking a list and multiplying all the numbers in it. This time I will assign 1 to the *special* value. > product = function(special,n) { return special * n } [Function] > special = 1 1 > list.reduce(product, special) 6

Monoids, binary operations and identity elements, oh my!

Wikipedia defines a monoid as:

an algebraic structure with a single associative binary operation and an identity element.

Product and sum are binary operations. And the operations sure look associative, by which I mean sum(2,sum(3,4)) = sum(sum(2,3),4) and product(2,product(3,4)) = product(product(2,3),4). [ 1 ]

All we need to make a monoid is an identity element. So what is an identity element?

It is an element that, when you apply the binary operation to it and any other element, the binary operation will always return the other element.

The special value is our identity element.

0 is the identity of addition; 1 the identity of multiplication. By which I mean x + 0 is always equal to x. And x * 1 is always equal to x, too. Or alternatively sum(3,0) == 3 and product(3,1) == 3.

Negative infinity

Astute readers will by now have realized that max is also monoid-like. And the really smart cookies will have spotted that its identity element is -Infinity.

If you think about it, max(-Infinity, x) is guaranteed to be x. Just as sum(0, x) is guaranteed to be x and product (1, x) to be x.

Implementing max goes like this. max = function(identity, num) { return num > identity ? num : identity }

And now we can find the largest number in the list. > list.reduce(max,-Infinity) 3

If we used any other number than -Infinity as our identity, we would run in to problems. If it were Infinity, then max would always return Infinity. If it were 0 then, if our list of numbers had only negative numbers in it, max would return 0. [ 2 ]

For the sake of completeness, we can define min similarly.> min = function(identity, num) { return num < identity ? num : identity } [Function] > list.reduce(min,Infinity) 1

In the real world

In the real world we might also choose to implement max with a for loop.

function max(xs) { biggestSoFar = -Infinity for(i=0; i < xs.length; i++) { biggestSoFar = xs[i] > biggestSoFar ? xs[i] : biggestSoFar; } return biggestSoFar; }

Note that we still need biggestSoFar to be -Infinity for our max function to work as expected. The logic is the same, but I find the functional version clearer. It is certainly shorter!

This is not too far off how max is actually implemented. Here is Google’s V8 implementation.function MathMax(arg1, arg2) { // length == 2 var length = %_ArgumentsLength(); if (length == 0) { return -1/0; // Compiler constant-folds this to -Infinity. } var r = arg1; if (!IS_NUMBER(r)) r = NonNumberToNumber(r); if (NUMBER_IS_NAN(r)) return r; for (var i = 1; i < length; i++) { var n = %_Arguments(i); if (!IS_NUMBER(n)) n = NonNumberToNumber(n); if (NUMBER_IS_NAN(n)) return n; // Make sure +0 is considered greater than -0. -0 is never a Smi, +0 can be // a Smi or heap number. if (n > r || (r === 0 && n === 0 && !%_IsSmi(r) && 1 / r < 0)) r = n; } return r; }


Note 1: Thanks to Michael Chermside for pointing out that I had originally described commutativity, not the associativity in this paragraph. Back.

Note 2: Thanks to andrew_404 for suggesting better wording for this paragraph. My original wording was misleading — for lists which contained a number >= 0 max would behave as expected. Back.


Comments

  • Be respectful. You may want to read the comment guidelines before posting.
  • You can use Markdown syntax to format your comments. You can only use level 5 and 6 headings.
  • You can add class="your language" to code blocks to help highlight.js highlight them correctly.

Privacy note: This form will forward your IP address, user agent and referrer to the Akismet, StopForumSpam and Botscout spam filtering services. I don’t log these details. Those services will. I do log everything you type into the form. Full privacy statement.