elm / core

Elm's core libraries
http://package.elm-lang.org/packages/elm/core/latest
BSD 3-Clause "New" or "Revised" License
2.8k stars 359 forks source link

(||) and (&&) not short-circuiting #495

Closed jvoigtlaender closed 8 years ago

jvoigtlaender commented 8 years ago

One might expect that there is no difference in behavior between this program:

import Graphics.Element

fib n = if n < 2 then 1 else fib (n-1) + fib (n-2)

main = Graphics.Element.show (True || fib 100 > 0)

and this program:

import Graphics.Element

fib n = if n < 2 then 1 else fib (n-1) + fib (n-2)

main = Graphics.Element.show ((||) True (fib 100 > 0))

But there is. The first program shows a result immediately, whereas the second program goes into a near-endless computation (the browser complains and wants to stop the script).

An analogous problem exists for (&&).

I think the documentation should warn about that. Maybe at several places (the syntax guide, when talking about operators; the core documentation).

It might even be worth considering to remove Basics.(||) and Basics.(&&) altogether, and only leave the || and && in as pieces that the compiler treats specially. (Apparently the compiler currently does treat them specially? Or how else is it explained that the expression True || fib 100 > 0 does not fire off that expensive fib 100 call?)

jvoigtlaender commented 8 years ago

Rationale for abolishing Basics.(||) and Basics.(&&) as functions (but keeping || and && as expression syntax, of course):

It will prevent people from accidentally writing inefficient code. For example, someone might currently cleverly write something like: oldWay = higherOrderFunction (flip (||) expensiveExpression). If (||) is removed as function, that someone would have to write newWay = higherOrderFunction (\arg -> arg || expensiveExpression) instead. Which is good, because newWay is much better than oldWay when higherOrderFunction applies its argument function to an expression that evaluates to True.

Can anybody show me, in Elm, a good use of (||) or (&&)? For example, there is good reason why List.any is not implemented as

any p = foldr ((||) << p) False

In Haskell, yes, but in Elm, no. So let's get rid of (||) and (&&).

mapmarkus commented 8 years ago

I've been exploring Elm's internals lately, and the behavior you describe might be the expected behavior given the underlying details (not to confused with the desired behavior).

Elm aliases operators like || or && to its native representation in javascript, so they keep their short-circuiting capabilities.

However, if you turn an operator into a function, elm compiler creates a wrapper around the operator, something like:

function (a, b) { return a || b; }

and because neither Elm nor javascript are lazy, the parameter b is evaluated before being sent to the operator (hence the behavior you observe).

In Haskell however, this will not happen because, Haskell being lazy, the parameter b would not be "looked at" unless its value is necessary.

We could devise a general solution, internally, using thunks when operators are used as functions, like

function (_a, _b) { return _a() || _b(); }

But honestly, this might be one of those things that are better solved by just adding a warning to the compiler that fires when certain operators are used as functions.

jvoigtlaender commented 8 years ago

I know why it is the way it is, functions being strict in Elm (but operators as syntax being compiled differently). That doesn't change the fact that the Basics module documentation of the functions currently raises different (wrong) expectations on the programmer's side. It also doesn't change the fact that I don't know of any good uses of the functions in Elm. Do you? If there are none, then the best solution is not to throw warnings when the operators are used as functions, but instead to not provide them as functions.

mapmarkus commented 8 years ago

I don't have any good example to be honest, but I don't think we need any.

The key idea is that, in general, Elm allows any operator to be used as a function. What you appear to be suggesting is to change this to any operator except || and &&.... Isn't that bringing underlying implementation details (how javascript works) into Elm's syntax (to the programmer side, as you say)? I though Elm, as a language, aimed to be as independent of the underlying javascript as possible.

jvoigtlaender commented 8 years ago

It's not bringing JS semantics into the picture, it's being true to Elm's semantics. In Elm all operators are strict, except those two. So we should acknowledge that these two are not functions, they are syntax.

Another take on this: What about if-then-else in Elm? It's currently only available as a syntactic operator, not as a function. By your "there should be no exceptions rule", there should be a function version of if-then-else. There isn't. I think that && and || should be treated like if-then-else, not like + and -. Because everything else is a "lie" (like the current documentation of (&&) and (||) in the Basics module is a lie).

jvoigtlaender commented 8 years ago

Of course, any programmer will still be able to define a function like

or x y = x || y

But then the semantics will be clear, and that programmer's responsibility, and they will not be able (and not tempted) to claim that these are short-circuiting, like currently wrongly claimed about (&&) and (||).

mapmarkus commented 8 years ago

Well, I don't think if then else qualifies as an operator. And the rule is not mine at all, is just my interpretation of how Elm's handles operators turned into functions. I might be totally wrong, of course.

jvoigtlaender commented 8 years ago

About the rule, yeah sure.

About if-then-else not being an operator, why not? Because it is ternary?

Anyway, I can rephrase my stance without mentioning operators: For me, && and || are not functions, they are syntax; just like if-then-else is not a function but syntax. So what is the reason that && and || must be available as functions?

rebelwarrior commented 8 years ago

Another way of solving it is having && and || not have direct function equivalents but have Basics.or and Basics.and to exist as functions analogues. So you would know they are not the same thing.

However, I agree with @jvoigtlaender that Basics.(&&) and Basics.(||) should be removed as methods (functions) from core if they don't have short circuit capabilities because that's what I'd expect and desire from those methods (functions).

altaic commented 8 years ago

Yikes! Short-circuiting is one of the most basic optimizations that is assumed to work in pretty much any language I've seen. This should either be fixed by using thunks (in which case the JS JIT might optimize the thunks away), or by writing an optimizer pass, or ditched altogether. Adding a warning only makes Elm seem flawed, which, to a new Elm developer, could be reason enough to walk away.

evancz commented 8 years ago

I don't think this is a mistake. All infix operators can be used as functions. Functions evaluate all their arguments before they are called.

Certain infix operators have short-circuiting. They are not functions, so the same rules do not apply.

I'm fine with how it is.

jvoigtlaender commented 8 years ago

But maybe at least a heads up in the documentation in Basics would be in order? That documentation currently reads like this:

(&&) : Bool -> Bool -> Bool

The logical AND operator. True if both inputs are True. This operator short-circuits to False if the first argument is False.

It says "this operator", and above that it is written "(&&)". So that's "this operator", right? Someone learning Elm has no reason to not assume from this that (&&) False exp will short-circuit. Or that passing "the operator" (&&) to another function will still provide short-circuiting. In your reply above you make a difference between operators and functions, and are of course correct about it, but people learning Elm will not necessarily be aware of this distinction, and specifically not here, where "this operator" refers to (&&) - which in your reply isn't actually the operator but the function.

I haven't made this problem up. It originally came up in a student's code. I can elaborate on what the problem was and why none of the current language and core documentation addressed it. But maybe I'll instead first make a more modest documentation change proposal than what I started this issue with. Maybe you'll like that proposal without further ado. Maybe not.

I propose to change the documentation to:

(&&) : Bool -> Bool -> Bool

The logical AND operator. True if both inputs are True. This operator, when used infix, short-circuits to False if the first argument is False.

So only adding the words "when used infix".