Compiler: Feature: 'where' expressions

Created on 6 May 2014  ·  85Comments  ·  Source: elm/compiler

I'd love to see support for "upside down" let ... in expressions, like you have in Haskell:

spam = foo bar where
  bar = ...

Sometimes code is easier to read if you see the Big Picture first, with the details hidden. Having a Haskell background, I often find myself laying out new definitions by typing let \n in first, leaving empty the let part, then sketching the Big Picture in the in part, and finally filling out all the details in the let part:

foo = let
      in

==>

foo = let
      in ... Big Picture ...

==>

foo = let details
      in ... Big Picture ...

I understand that 'where' expressions are a bit problematic because they flip the order in which code is executed (since Elm is eagerly evaluated, you'll have to evaluate the where ... block before evaluating the Big Picture expression). But I think this is something you can easily adapt your thinking to, so I wouldn't expect it to cause a lot of confusion.

Most helpful comment

I would gladly give up let for where :grin:

All 85 comments

where is easily the number 1 thing in Elm I want right now.

I would love to see these in Elm too. I have the same work flow.
And when I extend an already written function to something more extensive it's also easier to add a where.

But I don't think this will be a priority. let-in allows the same functionality and Evan is busy with the canonicalize branch, which will fix lots of bugs.
So if you really want this and have the time, you should implement it yourself and open a PR ;)

Before implementing things, it's important to define what the indentation actually means. What is the scope of my where clause exactly? This is something I find mysterious in Haskell and is like there to be a set of well defined rules before trying to implement stuff.

Where could be implemented by rewriting it to let and prepending it to the code. For instance:

fun = 
    let a = b in
    c
  where
    d = f
      where
        f = e
    g = i

means

fun =
    let g = i in
    let d =
        let f = e in
        f
    let a = b in
    c

In haskell it works like its implemented like this. I assume, that your parser thinks that if a is an expr, then is let b = c in a is an expr too.

Note that where starts a layout and could be inside another where.

I think, the popularity of this language for now depends on how precise it resembles haskell. Maybe, later it'll convince some BaconJS/ReactJS guys or anybody using some FRP.

The current decision is to not support where. I want to avoid adding redundant syntax for the foreseeable future.

I would gladly give up let for where :grin:

I think a lot of people agree with you @Apanatshka

Me too, @fosskers!

Me too, I find let very confusing.

I'm not sure whether what people are wishing for here is "where as in Haskell". But if so, they should take into account that not everything that can be written with let in Elm (or in Haskell) can be written with where in Haskell. Apart from the difference about whether the local definitions come before or after the main expression, where is strictly less general than let in Haskell. So in Haskell, at least, omitting let and keeping where would not make sense.

@jvoigtlaender Have you ever needed a let expression somewhere in Elm where a where clause would not have worked? And where factoring the code out into top-level definitions with where clauses wouldn't work, or would be a hack?
I haven't seen let being used anywhere where you wouldn't be able to use where instead. We already try to stay away from overly short variables too in Elm. So having the main expression first, and a where after that using sensible names makes more sense to me.

Not sure how to answer that, since I do not really mentally, consciously distinguish between programming in Haskell and in Elm, and I definitely like to be able to use local definitions scoping over expressions rather than whole function definition right-hand sides in Haskell, so I assume the same applies to me programming Elm.

Also, setting the bar at "where factoring the code out into top-level definitions ... wouldn't work, or would be a hack" is quite high. Of course, in some way it is always possible to do that, and whether it is a nice thing to do (or should be considered a hack) in a concrete case is a matter of taste, so difficult to judge.

Anyway, prompted by your question, I just had a look into Elm code I wrote this weekend, and this line is a place where I would not like to have to live without let. (I don't know whether it makes sense to ask how you would propose to write that function with where, because you would have to study that function first, and in any case, nothing in that module is very pretty. In a sense, most of it consists of quick hacking.)

I guess it really depends how you structure your code.

If you structure your code as: the lower you are in your file, the more specific (and possibly private) your functions get (i.e. the general, public API functions at the top), then where clauses make sense because you are replicating this structure within a single function.

But if you don't structure your file this way, then where clauses don't really buy you much. I have a more JS/Python/C background, and to me, the thing after the in is the return statement. It would just feel so weird to have the return statement be at the very top.

My opinion: either have let-in or where but not both. I personally prefer let-in because it is familiar if I treat the thing after the in as the return statement. But, just pick one cuz having both would be confusing.

Looking further down in that module, I find that I regularly use expressions of the kind (\x -> let y = ... in ...). Having only where, you would make me giving up (some uses of) anonymous functions or local definitions, because I would not be able to have local definitions inside a lambda-expression. That's expressiveness I don't want to miss in a functional language.

@jvoigtlaender Having let in that case is nice, but personally if my lambdas get too complicated I just relegate them to where. I much prefer brevity in the main line, a la:

foo :: (a,b)
foo = (a,b)
  where a = ...  -- something complicated
        b = ... -- something complicated

or

bar :: [a] -> b
bar = foldl f someAccVal
  where f acc x = ... -- complicated lambda

@fosskers, all very nice. I also do that (in Haskell). But only if I want to, not because the language forces me to. Which would be the case if the language takes away the possibility of combining local definitions with anonymous functions. I can only repeat that this is at stake here when discussing to replace let by where-a-la-Haskell: two useful functional features, local definitions and anonymous functions, would suddenly not anymore be usable together. Or do I miss anything?

I know this is all far into opinion land, but I think larger constraints on a language gives interesting results.
Like Elm has only the foldp primitive to get a stateful program, so you have to package all your state together in one big record. It sounds strange and bad, but it seems to be doing well. Perhaps distributing state more around a program will turn out to be an anti-pattern.

In the same way I'm thinking, maybe combining anonymous functions with local definitions is usually an anti-pattern. Wouldn't it be interesting to see if you get pushed to write more readable code by taking away the let and only supporting where? Your code style would have to change. And code style is always considered a subjective thing. But really that's only so because people loathe to do user studies (in general, afaict, I've never seen or heard of any) to see what kind of code is objectively more readable to humans.

When I have more time (probably in 8 hours or so), I'll see if I can rewrite @jvoigtlaender's linked Elm file in where style. Then we have a concrete example, see if it's a clearer code style. It won't prove anything, but it's a quick way to see if turns out to be the counter-argument that refutes my "theory".

@Apanatshka, if you rewrite the file, I'll be interested to look at the result. But I agree that it will prove little. And not just for the reason you mention, opinions, but also for another fundamental reason. Namely, "measuring" only readability of the final code ignores an at least as important aspect: how that code comes/came into being. And for practical, refactoring purposes, I think that declaring "combining anonymous functions with local definitions" an anti-pattern would be almost equivalent to declaring "using anonymous functions at all" an anti-pattern.

Why? Let's take a look at some other lines of said file: https://github.com/jvoigtlaender/Elm-Kurs/blob/e7db63ebaaa398f44a5431245fd227f11d5590cd/src/lib/Lib.elm#L130-L133. Let's say they started out as:

   Signal.map
       (\(x,y) _ t' state -> (t', { state | mousePos <- (toFloat (x1 + x), toFloat (y2 - y)) }))
       Mouse.position

In a world in which anonymous functions (alone) are not considered bad style per se, this should be a perfectly fine thing to write. Then, at some point I realise that I need to update the state.s component as well in there, and that I need the newly computed logical mouse position for that. So, what I need is:

   Signal.map
       (\(x,y) _ t' state -> (t', { state | mousePos <- (toFloat (x1 + x), toFloat (y2 - y)), 
                                            s <- upd NoEvent (toFloat (x1 + x), toFloat (y2 - y)) t' state.s }))
       Mouse.position

But I really shouldn't write it like that, since the duplication of (toFloat (x1 + x), toFloat (y2 - y)) is obviously bad style. So what I want is:

   Signal.map
       (\(x,y) _ t' state -> let pos = (toFloat (x1 + x), toFloat (y2 - y))
                             in (t', { state | mousePos <- pos, s <- upd NoEvent pos t' state.s }))
       Mouse.position

But in a world in which anonymous functions with local bindings inside are considered bad style (or even illegal according to the language definition), I would not be allowed to write that. Instead I would have to suddenly turn the anonymous function into a named one. Very bad, because the refactoring I want to do, abstracting the pos, is completely local to that function, so I should be able to do it in place, without changing the _outside_ or the _nature_ of that function (as an anonymous vs. named one).

If you show me a version of the above without an anonymous function with a local binding, this doesn't take the above "history" of the expression into account. The only way I can see I could have arrived at your potential version without going through the painful step in which an anonymous function has to be turned into a named one during an innocent and internal refactoring step, would be that I would have stayed away from using an anonymous function in the first place.

So: In a world in which anonymous functions with local bindings are bad style, but decent refactoring is desirable, anonymous functions should not be used at all. Because at any time they might turn foul because of an ostensibly beneficial refactoring step, causing much distress. (Well, that very last part maybe overstates a bit.)

I personally get nauseous when the main body of a function is more than a single line long, meaning by definition I don't like let statements as the following is common:

foo = let x = ...
      in ... -- very long expression

I argue that the following is less common:

foo y = let x = bar y in ... -- fits in one line

as this could probably be written more cleanly in point-free style. If the aim of the let is to use one value in multiple places, then a where serves just as well and frees up the main function body at the same time.

To each their own, but to me let itself is always an anti-pattern.

@fosskers, how does your suggestion "If the aim of the let is to use one value in multiple places, then a where serves just as well" play out inside an anonymous function? (Does it?)

Concretely, how would you write my example

Signal.map
    (\(x,y) _ t' state -> let pos = (toFloat (x1 + x), toFloat (y2 - y))
                          in (t', { state | mousePos <- pos, s <- upd NoEvent pos t' state.s }))
    Mouse.position

?

@fosskers, oh, I just saw that it was you who earlier said that you turn "complicated lambdas" into named functions. So I guess your answer might be you wouldn't write my example with an anonymous function at all. Which is fine. Just that having this enforced by the language would likely mean that one could rarely use anonymous functions at all (for long). Because at some point they may become "complicated" (needing a local binding inside), so have to be turned non-anonymous.

Because at some point they may become "complicated"

At that I would suggest that were the lambdas complicated enough, they would deserve being promoted to a named function anyway. My view is that anonymous functions are for short, one-off functionality.

I don't understand how this discussion can go so long without showing examples. I also notice that all the people involved know Haskell? Is this something that registers at all for a JS programmer? A JS programmer who has let as of ES6?

Furthermore, how will this interact with potential syntax for tasks? It's impossible to know right now.

I closed this issue because answering the most important questions are very hard and time consuming, and it's unclear that it'd be a big benefit compared to all the other things that can be done with that time and effort. If you want to pursue this further, the opinions don't matter. The code comparisons and use-cases should speak for themselves. If you can demonstrate those things, write it up in a gist in a clear way and share the link.

@evancz Can you re-read your post and consider if you find the tone berating? Maybe it's just my imagination, but I was in a good mood when I read it, so it's not my mood influencing things. Anyway, I know you don't mean it that way, maybe I'm just taking it the wrong way.

I also notice that all the people involved know Haskell? Is this something that registers at all for a JS programmer? A JS programmer who has let as of ES6?

We're talking about a feature that's well-known in Haskell. Obviously some people who know Haskell will find it easier to join this discussion. The JS programmer will need to learn some mechanism of binding names to values. Elm already has where for module definitions, so it's not that far-fetched. ES6 is getting fully supported by modern browsers but the average JS programmer will probably need to worry about backward compatibility. Also: ES6 let is a hack to get block scope into JS without breaking changes to var syntax. That's confusingly different from having an let _expression_ that kind bind multiple names simultaneously to immutable values.

Furthermore, how will this interact with potential syntax for tasks? It's impossible to know right now.

Interesting angle. But it's the _potential_ syntax for tasks, I haven't seen it on the todo list yet. And the done-deal but last minute postponed Signal/Stream split is now uncertain again, so I can't know if the same is true for the tasks syntax. I think the discussion here isn't ready to look at all the repercussions at once. Let's first continue with finding what the exact merits of where are.

I don't understand how this discussion can go so long without showing examples.

There are multiple small examples in the discussion, and a larger one is linked to. If you read carefully you'll see that I was planning on using that larger example for a side-by-side comparison.

I closed this issue because answering the most important questions are very hard and time consuming, and it's unclear that it'd be a big benefit compared to all the other things that can be done with that time and effort. The code comparisons and use-cases should speak for themselves. If you can demonstrate those things, write it up in a gist in a clear way and share the link.

We're just three people casually discussing this right now. I'm sure that if we come up with something concrete, we can recap it for the whole mailing list to discuss. This discussion just happened to end up on the issue tracker of your repo, but I don't think you should feel obligated to follow or join the discussion at this point. You're welcome to do so of course, but as we don't have anything concrete yet you can also just ignore it. :smiley:

@jvoigtlaender I got around to refactoring your Lib.elm in where style. It took me three revisions, and since I wrote this in a gist and where isn't supported I don't know if it's completely correct. I'm still not quite happy with the huge elaborateDisplay but I couldn't factor out any more without even larger refactorings, which would just make it harder to do the comparison.

As for the argument that anonymous functions can be used less without let-in, I agree, that diminishes the power of the anonymous function. But I look at it from the other side: let-in lures you into endlessly extending your anonymous functions into monstrosities that are not readable any more. You will have to refactor that anonymous function and give it a name at some point if you want to do a lot there and keep the code maintainable. Without let-in you just notice earlier that the time has come to say farewell to your once sweet little anonymous function :wink:

let-in lures you into endlessly extending your anonymous functions into monstrosities that are not readable any more.

This is what I was implying we should strive to avoid.

Is this something that registers at all for a JS programmer?

I think the JS programmer coming to Elm is going to know they're tackling a new language that resembles what they're used to very little. In my opinion, the logical leap to specifying "the details" below the main function body in a where is fairly low-friction.

This function may be the easiest to compare. The original:

makeGrid x1 y1 x2 y2 =
  let
    x_ = (x1 + x2) / 2
    xh = x_ - x1
    y_ = (y1 + y2) / 2
    yh = y_ - y1
  in group <|
   List.map (\i -> let x = toFloat i * gridsize - x_ in path' (dotted (Color.greyscale 0.15)) [ (x,-yh), (x,yh) ])
            [ ceiling (x1/gridsize) .. floor (x2/gridsize) ]
   ++
   List.map (\j -> let y = toFloat j * gridsize - y_ in path' (dotted (Color.greyscale 0.15)) [ (-xh,y), (xh,y) ])
            [ ceiling (y1/gridsize) .. floor (y2/gridsize) ]
   ++
   [ move (-x_,-y_) (Graphics.Collage.filled Color.red (Graphics.Collage.circle 2))]

And with where.

-- not exported
makeGrid x1 y1 x2 y2 =
  [ gridCoord x1 x2
    |> List.map (makeGridLine x_ yh)
    |> path' (dotted (Color.greyscale 0.15))
  , gridCoord y1 y2
    |> List.map (makeGridLine y_ xh >> swap)
    |> path' (dotted (Color.greyscale 0.15))
  , [ redDot |> move (-x_,-y_) ]
  ]
  |> concat
  |> group
  where
    x_ = (x1 + x2) / 2
    xh = x_ - x1
    y_ = (y1 + y2) / 2
    yh = y_ - y1

-- not exported
makeGridLine c1 c2 i = [ (c1',-c2), (c1',c2) ]
  where c1' = toFloat i * gridsize - c1

@Apanatshka and @fosskers, you both make the point that it may be good if the language, by forbidding local bindings in anonymous functions, forces me to turn anonymous functions into named ones sooner rather than later (or at all). That is certainly a defensible position. I may not like being forced to anything, but maybe it is for the greater good.

@Apanatshka, as expected, it turned out to be interesting to look at your refactoring of that code. I have a few observations:

  • Some of the names you gave to once anonymous functions are a bit off, from my perspective. For example, gridUpdate and gridRestart. This module as a whole is not really about grids. The grid just happens to be an auxiliary construct the users (beginning programmers) can blend in if it helps them. The real thing they are working with is a graphics plane or maybe game field, so maybe these concepts should have gone into these function names. Of course, you couldn't really know this, because you are not the author of the code and probably haven't studied it to that depth. So the point is only that as an author I would have come up with other names, after putting some thinking into it. I just avoided that lazily by not naming those functions (yet?).
  • You turned a lot of stuff into top-level functions. In contrast, I obviously have a tendency to not do this. In fact, if you were to look back through the commits of that file in my repo, you would find that I had more top-level functions originally, but later turned them into local functions. Generally, I did/do this for all functions that are used only at one place. For example, since makeGridLine (in your version) is used only in makeGrid, I would not want to have it lie around at top-level. Instead, I would put it inside the definition of makeGrid. It helps me to know that this function is used there and only there. Of course, this is a matter of taste, like in your other discussion.
  • But there is another point to this. Namely, as current your version will not work, because while moving out local functions (anonymous or not) to the top-level, you have missed a lot of dependencies. For example, gridUpdate accesses upd, which was in scope inside elaborateDisplay, but when you moved gridUpdate out to the top-level you forgot to explicitly pass upd along. Likewise, gridRestart accesses ini, so you would have to add a corresponding argument to the gridRestart definition and make sure to pass ini from elaborateDisplay to gridRestart. And so on. In some cases several arguments need to be passed on additionally. For example, mouseUpdate depends on upd, x1, and y2, which are in scope in toScreen where mouseUpdate previously lived, and would now all have to be passed from toScreen to mouseUpdate. I wonder whether if you were to revise your version further to fix these missing dependencies, you would like the resulting version less than your current one. This moving stuff out of its "natural (and only use) context" and paying the price of having to pass along dependencies can add a lot of clutter.

@fosskers, yes, but to the revised snippet you have to also add the definitions of gridCoord, swap, and redDot. Otherwise, the revised snippet is incomplete, and looks maybe more compact or shorter than the original one only by virtue of "cheating". (Not that I want to specifically defend the original version. As said earlier, I did not pay particular attention to readability when writing that code last weekend.)

+1, I find where clauses both easier to read and to write then let..in clauses. As a reader of a function with a where clause, I can focus on the high-level aspects of the function before diving into the details. As a writer I can lay out the skeleton of the function before filling out the "holes".

I disagree. Elm's philosophy has been "there's only one way to do it", which makes writing programs almost automatic (everything magically follows the Elm Architecture). It's a neat academic point that because of purity and immutability we can reorder our definition (and lazily evaluate them in Haskell), but I think let....in is fine.

Disagree with what? Some of the discussion above argues for _replacing_ let...in by where. Then the philosophy of "there's only one way to do it" would still be adhered to. So there's two proposals under discussion here:

  • Add where.
  • Add where and take away let.

Do you only disagree with the first of those?

But let x = a in b and (\x -> b) a are already two ways to do it.

I'm not seriously suggestion let...in should be gotten rid of, but "there's only one way to do it" is a rather vague notion in my opinion.

@polux, well, that's only part of the story, right? Because there is let {x = f y; y = z} in u, for which no equivalent with just an application of a lambda-expression exists. And vice versa, a stand-alone lambda-expression cannot be replaced by a let-expression. So the two are not interchangeable.

Yes, some syntactic subclass of let-expressions is interchangeable with some syntactic subclass of applications of lambda-expressions. But that's not the same kind of relationship as between let and where, where one completely subsumes all use cases of the other.

Let expressions are at least a special case of lambda applications, assuming you're ready to play tricks with fixpoint operators and tuples, aren't they? I didn't mean to be serious anyway, and I see your point, which I agree with.

@jvoigtlaender with the Y combinator you can write any let expression (recursive or not) as a series of lambda expressions.

@joneshf, I know. But, as at least @polux admits, that's beside the point in discussing the surface language (having or not having let and where and lambda-expressions). Or how, exactly, do you envision the Y combinator showing up in your Elm program?

I guess you can always circumvent the absence of where by writing

let body = ...
    x = ...
    y = ...
in body

That's a weird style but it addresses my issues with let.

@jvoigtlaender sorry, I thought you were rebutting the fact that you can write any arbitrary let expression as a lambda expression.

Or how, exactly, do you envision the Y combinator showing up in your Elm program?

I'm not sure what your question is here. Are you asking how to implement the Y combinator?

EDIT:

But, as at least @polux admits, that's beside the point in discussing the surface language (having or not having let and where and lambda-expressions)

I never said it was realistic or appropriate.

@joneshf, no, I'm not asking how to implement the Y combinator. I know all that. I teach it. Though, btw, you would indeed have a hard time implementing the Y combinator in Elm. Given that Elm is not the _untyped_ lambda-calculus and all that.

But the whole point of this discussion (read the many messages above if you haven't) is not about theoretic connections between these language concepts, and whether one feature (let expressions) can in principle be simulated by another. The discussion is exactly about what is "realistic or appropriate" in code meant for readability. So in that light I was refuting the idea that the presence of let in Elm could be "attacked" by observing that let is superfluous since it can be replaced by some convoluted uses of lambda expressions and fixpoints. (If you would be willing to go down that road, why having even lambda-expressions? Already the old supercombinator papers tell us that we can do without local bindings _and_ without lambda-expressions. But if you want to discuss all these relationships, that's decidedly not what the above discussion of the Elm surface language was about. It would be a nice academic exercise, but not very relevant for the issue under discussion here. I think.)

So in that light I was refuting the idea that the presence of let in Elm could be "attacked" by observing that let is superfluous since it can be replaced by some convoluted uses of lambda expressions and fixpoints.

Ah, gotcha. I understand now.

@fosskers I think we basically agree :smile:

@jvoigtlaender I agree with your points.
I didn't study the code well enough to find the best names for the functions.
Top-level functions is something of a code style preference. I like top-level helper functions because I identify a function with lots of local helpers as one large blob that needs to be understood completely. It also hides a lot of complexity by directly using local variables, and I guess I missed a bunch while pulling them to the top-level. You'll find in the revisions that I tried to pull displayGrid out of elaborateDisplay but couldn't without introducing way too many arguments.
Sometimes it's valuable to have a local function, but in my opinion they usually hide complex dependencies and you're forced to write more cleanly separated code when you disentangle them. But as I said, I didn't want to go that far with the refactoring :) Anyway, I think we understand each others preferences and opinions by now.

@Apanatshka and @fosskers, you both make the point that it may be good if the language, by forbidding local bindings in anonymous functions, forces me to turn anonymous functions into named ones sooner rather than later (or at all). That is certainly a defensible position. I may not like being forced to anything, but maybe it is for the greater good.

I guess that's the question. The design decision is whether this is a warning from a lint tool that recommends a better code style or if you constraint the language to not allow (one form of) "bad code". Regardless, I don't think it would be easy to convince Evan of the benefits here, considering his reaction to this discussion. I'm not interested in leading an attempt to convince Evan to do this experimental change in hopes of improving general code readability. But I certainly enjoyed discussing this with you. So thanks @jvoigtlaender, @fosskers and others. This was interesting.
(Of course if someone else wants to try bringing this idea into Elm, I'll gladly support that. But I don't have the bandwidth for it myself, I should be working... )

Last words in a nutshell.

Where:

  • is an intuitive concept
  • leads to cleaner code than let
  • is (anecdotally) more popular

Let:

  • has it's place with record syntax in Elm

Evan seems to have his mind made up about this, but no one should be surprised to see a stream of "+1 yes Elm needs where" posts in this thread for the next years.

I'm very used to where syntax in Haskell. Without going into details, would it be at least possible to have a reversed let variant? That is, remove the let --> in order requirement?

So, besides:

function =
    let these variables
    in this expression

We could also do:

function =
    in this expression
    let these variables

I can see that causing confusion.

It is a poor substitute for where for sure, but all that it means is: the order of let and in doesn't matter.

I like the in ... let proposal :)

A problem can be approached either top-to-bottom (where) or bottom-to-top (let). Functional programming itself lends towards the former, that's why I would prefer having only where or both where and let in Elm.

where

+1 for where regardless of keeping let..in or not

because it's closer to the "declarative" style of coding. Elm is FRP, FRP is declarative by its nature, then why not support where?

@evancz

I also notice that all the people involved know Haskell? Is this something that registers at all for a JS programmer? A JS programmer who has let as of ES6?

I believe that this should not be an argument. Because we definitely are talking about FRP/declarative style of programming and common JS programmers are nevertheless far from these ideas.

Just consider this official example from elm architecture tutorial:

view : Signal.Address Action -> Model -> Html
view address model =
  let counters = List.map (viewCounter address) model.counters
      remove = button [ onClick address Remove ] [ text "Remove" ]
      insert = button [ onClick address Insert ] [ text "Add" ]
  in
      div [] ([remove, insert] ++ counters)

As for me this is pretty good example of "have to read from the bottom to top" which is pretty uncomfortable, because it is against to the normal "reading flow" - from top to bottom, and against the declarative abstract programming, when first you tell about some abstraction, and then describe details...

All the time when playing with clojure I felt this "tiny pain" when opening each file of code and scrolling to the end to start reading it from bottom to top.

There is one idea that the code is good when it tells a good story. And "bottom to top" does not help with this much...

The following tells a better story:

view : Signal.Address Action -> Model -> Html
view address model = 
    div [] ([remove, insert] ++ counters)
where
    remove = button [ onClick address Remove ] [ text "Remove" ]
    insert = button [ onClick address Insert ] [ text "Add" ]
    counters = List.map (viewCounter address) model.counters

I understand that this may be a tricky task. Because it's really not obvious to what "previous" context should where apply, and how to deal with indentation.

From other point of view maybe where should not be as powerful as let .. in. Because too much of nested "where-s" will end in too complicated code...

Actually I see the need for where only in describing in declarative way - the functions and data that is needed only inside one function/expression. I don't see that "nesting support" will help much with this. This can make the "task" much easier to implement.

The other way of implementation may be to go the "let..in" way in context of "using two words".

E.g. design it like:

view : Signal.Address Action -> Model -> Html
view address model = 
    return 
        div [] ([remove, insert] ++ counters)
    where
        remove = button [ onClick address Remove ] [ text "Remove" ]
        insert = button [ onClick address Insert ] [ text "Add" ]
        counters = List.map (viewCounter address) model.counters

Just "return" seems to be "bad" choice in case Elm will use return the same way haskell uses it. But haskell's return - seemed nevertheless awkward to me... So... maybe it's a good choice to use it as a starting point for where scope in Elm.

Currently no any other proper words comes to my head... Any ideas?
do .. where ?
explain .. where ?
the .. where ?
this .. where ?
give .. where ?
take .. where ?
is .. where (but this will be tautologic a bit in ... = is ... where ...

I closed this issue because answering the most important questions are very hard and time consuming, and it's unclear that it'd be a big benefit compared to all the other things that can be done with that time and effort.

Yes, for sure there are many tasks of much higher priority. But this means that the "where support" should be postponed (e.g. via marking with correspondent label), but not closed and forgotten...

+1, the mental flexibilty 'where' provides makes coding more pleasant. Even nicer if the compiler supported swapping from 'let' to 'where' for IDE's.

Have you ever worked on a large Rails app and seen the same thing done 10 different ways? That's what's going on with let/where. As soon as there's a subjective decision, codebases fragment, and developers lose time on petty arguments. There has to be one way to do it.

I agree that too many ways is bad, but that doesn't mean that 2 is too many. I think the right number of ways to do something in a language should be between 1 or 2, but no more than that. 10 is madness. I like how Elm handles record type aliases for example, there are 2 ways to construct them and each facilitates a different use-case.

I think it is pretty well acknowledged that 'where' is a much more popular and readable alternative to 'let' (when it can be applied). There's no reason that having 'where' in Elm is going to cause arguments or fragment code bases; so I don't know what all that was about.

The main problem with 'where' is that Elm tries to replicate Haskell syntax. By not implementing what is a very fundamental syntactical construct, it is guaranteed that Haskell users are going to be upset, when they end up trying to use it. This adds to the learning curve of using Elm, as well as other issues:

  • Having to always snap out of using 'where' when switching from Haskell into Elm
  • Not being able to write programs following the natural reading order (illustrated earlier)
  • Making it harder to port Haskell code into Elm (which already uses 'where')

As noted previously in this thread, 'let' and 'where' aren't even semantically equivalent, so having 'one way to do it' is actually not an option here. This is simply a missing feature, which should really be implemented. But, as far as I could tell, the lead developer hasn't really looked into how it would be done (the same as Haskell?) and isn't interested in doing it.

I expect that people are going to continue to complain about the absence of 'where' until one of the following happens:

  • Someone makes a fork of Elm which does implement 'where', and everyone starts using that (it's more usable) - at which point you're forced to introduce it - or risk losing control of the project and fragmenting the community (the joys of open source)
  • Someone makes something better than Elm; Elm fades into obscurity and therefore nobody talks or cares about it any more

So... having this issue being closed really doesn't achieve anything. Probably nearly all of Elm users would prefer to have this syntax available. So... make it happen already!

By the way there is quite a good discussion of this now on the Haskell Wiki succinctly describing the key differences between 'let' and 'where'. I think it makes it quite obvious that both are needed, since sometimes you can only use one and not the other.

That Haskell Wiki page doesn't prove anything concerning the necessity of having"where" in Elm. The case it gives for when you cannot use a let-expression does not apply to Elm, which has no guarded equations. So, no, that page does not make it obvious that "where" is needed _in Elm_.

Several of your other arguments are moot as well.

For example, Elm is _not_ trying go replicate Haskell syntax. That statement is simply wrong.

@ColonelJ I think you are being abit sour. Elm is a unique project with its own voice, and not trying to replicate Haskell. Its done great so far without 'where' and likely will continue to succeed without adding 'where'. I personally would preffer the addition of 'where', but its not going to hold Elm back much.

On the point that Haskell users will dislike Elm for not having where, _Haskellers are not Elm's target audience, JS programmers are_. And coming from an imperative/procedural world, let makes a lot more sense and where seems "backwards".

I believe that Elm's subject seems to be closer to "declarative reactive
functional programming". And target audience is what will be a consequence
of the former. Declarative and FRP is nevertheless a completely other world
from imperative/procedural. So why care of the latter? :)

"where" is closer to declarative style than "let".

Нд, 22 трав. 2016 о 20:28 Max Goldstein [email protected] пише:

On the point that Haskell users will dislike Elm for not having where, _Haskellers
are not Elm's target audience, JS programmers are_. And coming from an
imperative/procedural world, let makes a lot more sense and where seems
"backwards".


You are receiving this because you commented.
Reply to this email directly or view it on GitHub
https://github.com/elm-lang/elm-compiler/issues/621#issuecomment-220844777

I think any effort to use Elm is much more likely for someone who's already sold on functional programming, than a Javascript developer, who already has a lot of tools at their disposal. Compared to a language built into every browser (with full access to webpage elements), a Javascript developer would have to have a very open mind to use something like Elm, which requires a totally different ecosystem (built under Haskell no less!)

For a Haskell user, on the other hand, Elm lets you do Web client-side stuff, which you couldn't before, and more importantly, it lets you explore the world of FRP (more likely the reason you decide to play with Elm in the first place, from a theoretical perspective).

Since having a Haskell-like thing to run in the browser is nice, changes from the Haskell syntax are best avoided. Though one has to acknowledge that the <| borrowed from F# is a lot better than Haskell's $ when provided alongside the popular |> (with people even adding these to Haskell). Other stuff, like swapping the meaning of : and :: (from F#, Scala, etc.), tuple constructors (,,), and so on, we can live with, but a lot of other differences just show up as omissions from Elm or are just plain annoying/confusing. (E.g. I don't agree with << instead of ., which represents the circle composition operator from maths. There's already >>> for the other option in Haskell - why need a different syntax?)

I found this very interesting list of differences between Haskell and Elm. Thanks to Python making list comprehensions very much the 'mode du jour' (being far more readable for long expressions), the whole map/reduce idea has lost favour, and this is another Elm missing feature which shows up just like where in the above list. (Of course there's an issue for it already #147 which is also CLOSED.)

And right, the special use case for where being needed in Haskell. Indeed, that doesn't apply, because Elm doesn't have guard expressions... Great, yet another feature that Elm doesn't have! Maybe it should be added too? (And forget about the multi-way if syntax. This is NOT the same as pattern guards, and is probably going to be removed anyway.)

Coming back to the "only one way to do it" logic, by excluding where from Elm, one might wonder why you wouldn't also consider removing <| and << for exactly the same reason: they don't follow the imperative control flow, and you can use |> and >> instead. Makes sense right, who needs 'em?

Man, I don't where this language is going, but implementing more features of Haskell into Elm is better than not, and the syntax doesn't need to change. If you can just use some 'Elm equivalent' it's not so bad, but let is no substitute for where.

@ColonelJ, you are entirely entitled to your own opinion about what the target audience for Elm _should_ be, but that doesn't change a _bit_ about what the declared target audience for Elm _is_. You think Elm shouldn't try to "convert" JavaScript developers. Fine. The creator of Elm has a different opinion, and certainly you cannot expect that because of the opinion you have the language changes course.

Haskellers are not Elm's target audience, JS programmers are.

Given that Haskellers were the initial adopters, Evan and the rest of the Elm core team have to realize they alienate their original users by taking this attitude.

All power to Elm if its goal is to "win" the frontend stack race. I really hope that happens. It's unfortunate that Elm thinks it has to be "easy" and must avoid advanced concepts at all costs in order to be useful/adoptable.

Designing to infantilize a target audience is how we got Java.

JavaScript dev's who are interested in Elm, are the cream of the crop imho and good engineers will step upto the plate when challenged. I love how Elm lowers the barrier to entry, but that is a separate concern from target audience. It feels like its being implied that good JS devs are less willing or capable of dealing with abstraction than Haskell devs, and its just not the case. JS world is maddeningly complex, and good JS engineers deal with complexity way beyond what a simple 'where' clause introduces.

I'd like to see Elm focus on being the best programming language in can be, and meet the goals of the language, including simplicity and a low barrier to entry. But that is very different from, 'lets exclude useful abstractions because JS devs are too dumb to understand', that's the wrong attitude. Not to say there are not stupid JS devs out there, but Elm should not en-devour to fix the unfixable.

If the addition of language feature X, makes Elm for flexible and pleasurable to use, and doesn't conflict with the goals of the language or the preservation of its properties.... then it makes Elm more flexible and pleasurable to use, period; regardless of the programming background of the developer.

@Fresheyeball, I fully agree with you. But what we are getting above is wrong statements like "Elm tries to be a Haskell derivate, so it must add this and this and this syntactic feature from Haskell". Or "It's basically hopeless to make Elm attractive for JS folks, so the language must target Haskell folks instead, and for this reason must copy as much as possible of Haskell syntax, because things must be like they are in Haskell, in order not to turn away Haskellers" etc.

I still vote for where. But not solely because it is present in haskell - that is not the root cause. I vote for this but because it allows me to make things upside-down. For now, Elm allows only downside-up constructions.

Downside-up code breaks logical flow. You read: problem deps... = let now stop and see how I shift bits in ... and think "wait, that doesn't looks like a solution... oh, it is (10 lines below)".

Elm tries to be a Haskell derivate

It's changed quite a bit over the years, but we can't deny the stark similarity between the two. Don't want higher-kinded types? Fine, all power to you, Elm doesn't have to be Haskell. That said, drawing the line at where vs let using that same argument seems silly.

It's basically hopeless to make Elm attractive for JS folks

Is anyone actually claiming this? Adding where to appease the Haskellers is not anti-JS nihilism. Adding where isn't even just for the Haskellers. where _is just better than let_.

I assume the last comment is addressed to me, since it is quoting excerpts from my previous comment. If it is addressed to me, you seem to be making assumptions about my position that are not true. I have nowhere above said that I am against having where. If you look through the old comments above from months ago, you will find that I was arguing against _replacing_ let by where, not against _adding_ where. And if you look throught the comments from the recent days, you will find that I didn't take a position about having or not having where at all. Nor do you know anything about my preferences concerning higher-kinded types or whatever. All I was arguing against was claims that Elm _must_ add where, "because Haskell".

About this:

It's basically hopeless to make Elm attractive for JS folks

Is anyone actually claiming this?

I was writing that because of this paragraph in someone else's comment:

I think any effort to use Elm is much more likely for someone who's already sold on functional programming, than a Javascript developer, who already has a lot of tools at their disposal. Compared to a language built into every browser (with full access to webpage elements), a Javascript developer would have to have a very open mind to use something like Elm, which requires a totally different ecosystem (built under Haskell no less!)

... followed by essentially an argument that "because of this", Elm must copy Haskell as much as possible in syntax and operator names.

I was quoting you, yeah. That said:

you seem to be making assumptions about my position that are not true

More like the de facto agreement of the Elm team, as opposed to you specifically. The same goes for higher-kinded types. I didn't mean you. I've just reread the entire thread, and it's interesting that it has been an entire year we've been debating this.

All I was arguing against was claims that Elm must add where, "because Haskell".

We can agree that that's silly.

I was writing that because of this paragraph...

Point taken.

I was going to suggest more discussion points, but having reread the entire thread as I was writing this, I can see almost everything that can be said has been. Critically, however, the following has not yet been discussed, and has been a concern of mine recently (as a user, contributor, and proponent of Elm):

Designing to infantilize a target audience is how we got Java.

I'm from JS and I'd like to use where over let, for the reason @Heimdell gave.
JS has already gone to (x) => x * 2, so it shouldn't be too hard for a JS programmer to learn in Elm what would amount to (x) => x * y where y = ....

let/in goes against elm style guide

Goal: a consistent style that is easy to read and produces clean diffs.

Haskell:

Before:

f a b = a + b

After new feature:

f a b = a + b + c
  where c = 10

image
Easy to read changed body (it is immediately after the old).

Elm:

Before:

f a b =
    a + b

After new feature:

f a b =
    let
        c = 10
    in
        a + b + c

image
It is like a new function with old name

@kana-sama Please correct me if I'm wrong, but isn't that just about Elm having a newline after f a b =, rather than about let vs where?

@willnwhite Nope.
image
Diff is much cleaner after adding where than after adding let-in, because a + b + c comes immediately after a + b.

Got it! I was interpreting function body as including the let-in, so I couldn't see it.

Folks: where-expressions are not going to happen. A contrived diff isn't going to make a difference.

(Yes, contrived: how often do you add a new parameter like this when making a change? You have a solution in search of a problem. When designing Elm, we like to start with a collection of real-world pain points, not a collection of language features.)

@mgold the examples are coming in because Evan likes them. Also, since you need painpoints, here's mine. I thought i'd never share it since it makes me uncomfortable confessing it in public, but what the hell. It's for a higher cause.

I have a medical issue where once I open a file and start reading it, I can't go back. If i need to read a couple of lines above, I need to close the file, reopen it, and start over, hoping i wouldn't miss them again.

I don't need to tell you how bad let..in makes things for me.. needing to remember everything inside let, just so i can use it inside in. Countless times have I forgotten just because there was one too many line inside the let for me to remember. It's a good thing I have a shortcut for reopening files.

And doctors keep telling me how there is js compiler for haskell, but i stand my ground! And I will. keep. standing. my. ground.

@Birowsky I'm sorry to hear about your condition. Thank you for having the bravery to share.

I don't know how to balance your medical needs with the (non-medical) needs and preferences of the rest of the community, so I will bow out of this conversation.

@mgold Yes, contrived: how often do you add a new parameter like this when making a change?

You are referring to the adding of c in this code?

f a b =
    let
        c = 10
    in
        a + b + c

There is no extra (function) parameter in the above; and isn't it common to abstract out a subset of expression and assign it to a variable for re-use? I do it all the time (using where in PureScript, or let in Elm).

When designing Elm, we like to start with a collection of real-world pain points

If I may ask? What is the (data) source for these real-world pain points if not the barrage of requests and complains in this Github Issues tracker?

isn't it common to abstract out a subset of expression and assign it to a variable for re-use?

This is precisely the purpose of both let and where. The written out characters from the above example were contrived, but the example itself was not.

Yes, it's common enough to extract named expressions, and I may have been fooled by the generic-ness of a, b, and c. However, once a let (or Haskell where) is in use, extracting another item creates a simple diff in both cases.

If I may ask?

I may be grouchy on this thread, but this community never turns away an honest question!

What is the (data) source for these real-world pain points if not the barrage of requests and complains in this Github Issues tracker?

They're not data in any statistical sense, but we like to hold language discussions on the mailing list. The best of these focus on "how do I do this web development thing" or "how can I structure my application". This issue tracker is used by Elm's creator for bugs, not feature requests.

Continued on the mailing list: https://groups.google.com/forum/#!topic/elm-discuss/KiKF6K9gBKU

Miso has where. 😉

nothing yet about this feature? there's only intended support to let ... in expressions as it is?

@Mazuh see the mailing list link. Here's a summary of the arguments on both sides.

Note: Elm's creator didn't participate in any of the follow-up discussion (his last comment was in this GH thread in 2015), and considers the matter closed.

I Haskell in the frontend nowadays and make both use of let and where. The
intent-first thing is my primary motivation to use where clause.

Le dim. 22 juill. 2018 10:07 p.m., Colin Woodbury notifications@github.com
a écrit :

@Mazuh https://github.com/Mazuh see the mailing list link. Here's a
summary of the arguments on both sides.
https://groups.google.com/forum/#!msg/elm-discuss/KiKF6K9gBKU/jzMuSTZmEwAJ

Note: Elm's creator didn't participate in any of the follow-up
discussion (his last comment was in this thread in 2015), and considers the
matter closed.


You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/elm/compiler/issues/621#issuecomment-406918553, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AAAPngtOGr2LPLRBErGt5e5RmTmXcFl-ks5uJS_dgaJpZM4B4ePC
.

Hey folks, we're not gonna do where. Please see https://discourse.elm-lang.org/t/why-does-elm-support-let-but-not-where/1634/7 for an explanation.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

avh4 picture avh4  ·  3Comments

maxsnew picture maxsnew  ·  4Comments

torepettersen picture torepettersen  ·  4Comments

evancz picture evancz  ·  4Comments

zoren picture zoren  ·  5Comments