4 ms·
Reposted from https://news.ycombinator.com/item?id=12530652 https://news.ycombinator.com/item?id=12530652 I disagree with some of the claims, as I think these
by chriswarbo 10y ago
Reposted from https://news.ycombinator.com/item?id=12530652 https://news.ycombinator.com/item?id=12530652
I disagree with some of the claims, as I think these sorts of edge-cases mostly serve to highlight how imprecise the given pure/impure distinction is, and how we can't always take a semantic concept from one language (e.g. Haskell) and apply it in another (JS).
For example, if you always get "TypeError" when calling "sum()", you can replace "sum()" with its result ("throw TypeError") and get the same behaviour; it doesn't break referential transparency, so is it "pure"? What do we even mean by "result" in a language with exception handling? Do thrown exceptions count, or only return values?
Similarly, using "valueOf" doesn't show that the sum function is impure, it shows that the sum function is higher-order. If we mapped 'Math.random' over an array, does that make 'map' impure? What does it mean to be pure/impure when any variable can be replaced by a 'valueOf' function/procedure?
If two expressions call the same procedures, the same number of times, in the same order, and return the same pure function of their outputs, are those two expressions the same? Would replacing one with the other maintain referential transparency, and does that imply "purity"?
I think the answer is that such examples need more fine-grained notions than just "pure"/"impure". Otherwise it's just arguing over the semantics of English, rather than semantics of programs.
- jerf 10y agoDefining a pure function as "a function that is pure as long as you use it purely" isn't a very useful definition. (Note distinction between "not very useful" and "useless".) It isn't very useful to humans, who do not have the cognitive bandwidth to process the resulting graph of purity and impurity through any non-trivial code base, especially in dynamically-typed language like Javascript where the purity of a function's invocation can be changed at runtime with the full arbitrariness of a Turing-complete language. It isn't useful at all to compilers or JITs, who if they can not be assured a function is definitely, 100% always pure must always check in one way or another and thus lose optimization opportunities. It's sort of useful in terms of looking at leaf nodes of the structure of the program and determining that this call is pure (today, at least) and thus reasoning with it hyperlocally, but that's pretty much it, and that's not that great of an advantage over what programmers already do. And such reasoning is still quite weak given that it can be invalidated by changes to the local environment, like complicated objects getting passed in. I think purity isn't too confusing here, and it's not really a Haskell concept being used, it's general purpose. It's just that the answer given by the post is correct; it's essentially impossible to write pure Javascript code. It's also, for much the same reasons, impossible to write pure Python, Ruby, or Perl; there's so many ways to invoke something that runs arbitrary unconstrained code (properties, method overrides, class-level hacks, metaprogramming, redefinitions of symbols used by the function) that it's very difficult or impossible to write pure code. (I think you could write a pure id function in JS. But in light of the fact that x + 10 can be impure, that would seem to be about the limit.) And this fact is not just academic; it's part of the reason why it seems like JS engines have plateaued at around 8-10x slower than C. These issues may be academic to most JS programmers, but they are brutally, unavoidably practical to someone writing a JIT.
- chriswarbo 10y ago> I think purity isn't too confusing here, and it's not really a Haskell concept being used, it's general purpose. It's just that the answer given by the post is correct; it's essentially impossible to write pure Javascript code. I completely agree, especially when comparing languages. But if we're focusing on a particular language, like JS, then it's not particularly useful to make distinctions like "pure"/"impure" which essentially lump everything into one category. Once we've said "it's all impure", there's not really anything else to say, unless we make new distinctions like the ones I made above.
- jerf 10y agoAh, yes, I agree completely. Definitions that bin everything into one bin are useless. I'm not sure there's a useful definition of pure for JS, though. (Or Python, Ruby, or Perl, or anything else that dynamic.) The one where you just sort of wing it and say "Yeah, function (x) { return x + 10 } is pureish enough" is really tempting, but it will stab you in the back sooner or later, guaranteed.
- catnaroek 10y agoRather than talking in terms of “purity”, it's better to talk in terms of “effects”. Raising an exception is an effect. Returning different results when passed the same arguments twice is an effect. I/O is an effect. Looping forever (also known as “divergence” in some circles) is an effect. What you call “purity” is just the absence of effects. From the point of view of programming language semantics, what all effects have in common is that they reduce the extent to which equational reasoning is applicable to our programs. For example, f(x) == f(x) doesn't always hold if `f` is a non-deterministic procedure, and function foo_bar() { var x = foo(); var y = bar(); return (x, y); } isn't the same as function bar_foo() { var y = bar(); var x = foo(); return (x, y); } if `foo` and `bar` have effects that don't commute. --- Higher-order functions also give us an interesting possibility: effect polymorphism. Let's consider the humble `map` function, operating on lists: map _ [] = [] map f (x :: xs) = f x :: map f xs Intuitively, it's clear that `map f` is an effectful procedure if and only if `f` is an effectful procedure. This notion can be formalized by giving `map` the following type signature: forall (a b : Type) (f : Effect). (a -f-> b) -> (List a -f-> List b) Where `Foo -Bar-> Qux` means “procedure with argument type Foo, effect Bar, and return type Qux”, and `Foo -> Bar` is sugar for `Foo -Pure-> Bar`.
- chriswarbo 10y agoAlgebraic effects are a way to make well-behaved languages (Haskell, Idris, etc.) more intuitive/generic/terse/etc. to write; I'm not sure how useful they are for reasoning about an arbitrary snippet of JS taken from the wild. One problem stated in the article is we can't even rely on things like variable substitution to be effect-free, due to mechanisms like "valueOf". For example, in your "map" definition, the value of "f" called at the head of the list may differ from the value of "f" passed to the recursive call. With this level of monkey-patching possible, it becomes quite hard to say anything about a piece of code other than "it's return type is Any, it might throw an exception, and it could perform arbitrary effects" :(
- catnaroek 10y ago> I'm not sure how useful they are for reasoning about an arbitrary snippet of JS taken from the wild. Probably not much, indeed. > One problem stated in the article is we can't even rely on things like variable substitution to be effect-free I have no idea how to make sense of this. The presence or absence of effects is an object language notion. Variable substitution is an operation on the syntax of this object language, and syntax necessarily lives in a metalanguage (e.g., the language in which a compiler or interpreter is written). EDIT: Forgot about explicit substitutions, which make substitution an explicit reduction step in the object language. Even then, I don't see how substitution can be effectful by itself. > For example, in your "map" definition, the value of "f" called at the head of the list may differ from the value of "f" passed to the recursive call. At least in a call-by-value language, this is should never the case. Funny things can happen if lexical environments are first-class objects, of course.