This may border on philosophical, but I thought it would be the right place to ask.
Suppose I have a function that creates a list of IDs. These identifiers are only used internally to the application, so it is acceptable to use ES2015 Symbol()
here.
My problem is that, technically, when you ask for a Symbol, I'd imagine the JS runtime creates a unique identifier (random number? memory address? unsure) which, to prevent collisions, would require accessing global state. The reason I'm unsure is because of that word, "technically". I'm not sure (again, from a philosophical standpoint) if this ought to be enough to break the mathematical abstraction that the API presents.
tl;dr: here's an example--
function sentinelToSymbol(x) {
if (x === -1) return Symbol();
return x;
}
Is this function pure?
Not really, no, but it might not actually matter.
On the surface,
(foo) => Symbol(foo)
appears pure. While the runtime may do some operations with side effects, you will never see them, even if you callSymbol()
at the same time with the same parameters. However, callingSymbol
with the same arguments will never return the same value, which is one of the main criteria (#2, below).From the MDN page:
Looking solely at side effects,
(foo) => Symbol(foo)
is pure (above the runtime).However, a pure function must meet more criteria. From Wikipedia:
You could argue the preface to that list rules out everything in JavaScript, since any operation could result in memory being allocated, internal structures updated, etc. In the strictest possible interpretation, JS is never pure. That's not very interesting or useful, so...
This function meets criteria #1. Disregarding the result,
(foo) => Symbol(foo)
and(foo) => ()
are identical to any outside observer.Criteria #2 gives us more trouble. Given
bar = (foo) => Symbol(foo)
,bar('xyz') !== bar('xyz')
, soSymbol
does not meet that requirement at all. You are guaranteed to get a unique instance back every time you callSymbol
.Moving on, criteria #3 causes no problems. You can call
Symbol
from different threads without them conflicting (parallel) and it doesn't matter what order they are called in.Finally, criteria #4 is more of a note than direct requirement, and is easily met (the JS runtimes shuffle everything around as they go).
Therefore:
Symbol()
is definitely not pure, thus the example is not either.Yes, this function is impure:
sentinelToSymbol(-1) !== sentinelToSymbol(-1)
. We would expect equality here for a pure function.However, if we use the concept of referential transparency in a language with object identities, we might want to loosen our definition a bit. If you consider
function x() { return []; }
, is it pure? Obviouslyx() !== x()
, but still the function always returns an empty array regardless of the input, like a constant function. So what we do have to define here is the equality of values in our language. The===
operator might not be the best fit here (just considerNaN
). Are arrays equal to each other if the contain the same elements? Probably yes, unless they are mutated somewhere.So you will have to answer the same question for your symbols now. Symbols are immutable, which makes that part easy. Now we could consider them equal by their [[Description]] value (or
.toString()
), sosentinelToSymbol
would be pure by that definition.But most languages do have functions that allow to break referential transparency - for example see How to print memory address of a list in Haskell. In JavaScript, this would be using
===
on otherwise equal objects. And it would be using symbols as properties, as that inspects their identity. So if you do not use such operations (or at least without being observable to the outside) in your programs, you can claim purity for your functions and use it for reasoing about your program.