Elm shadowing change issues

Forgive the long preamble, but I think the context is important. It’s been a good few months since shadowing was outlawed and I’ve been trying to follow the shadowing advice for renaming variables. My usual workflow when creating functions is to have a let..in block at the top. I then go to work in the body of the function. Whenever I have written enough code to think “ah, yes, I can name this!”, I put it into the let..in area. During the writing of additional parts, I sometimes find that I need to change the earlier code (or even types) and so I modify the let..in block. Later on, I separate out some of the let..in block into separate functions. Think of the let..in area as a place where you can be mutated, promoted to top-level, or killed entirely, depending on how things work out.

This means that when I make functions in the let..in section, I am often working with related data. Right now, I’m doing something with “properties” (which mean something domain-specific, don’t get hung up on the name), so I pass in something to the top-level function and name it prop. Now I have the following pain point: I can’t use prop anywhere else. As a result, I’ve got all sorts of rubbish names: prop_, property, currentProperty, newProperty, p, and so on. Could I choose better names? Well, yes. One can always choose better names. Could I choose better names, while still staying in the flow of what I’m doing and not losing track of the overall objective of the code? Experience has shown me that the answer is “no”. If I try to, I lose my focus and my productivity takes a hit. Instead, I have a fixed set of mental prefixes and suffixes that I apply: _, new, the, a, cur, current, my, this, etc. Maybe I’m not smart enough or not multitasking enough or something, but after months of trying, this is where I’m at. More design would help, but no, I don’t always do a full design to this level of detail up-front; in all honesty, 99.9% of programmers that I know don’t do so either. As it is, I do a lot more design than most.

Annoyingly, once I actually split things out into helper functions, I know that this problem will go away because scopes will change. And then I’ll go through and change prop_ etc everywhere to prop, which is the naming convention that it should have been obeying in the first place…

Here are some things that I’ve tried:

  1. Don’t compile the code until it’s in its “finished” form. This has all of the drawbacks of not-compiling-code that I’m sure that you’re aware of and, just like a chef who doesn’t taste their food while it’s being made, it sometimes leads to worse problems.

  2. Split things out into top-level functions early. This is a bit more work and it sometimes breaks my focus. Type signatures are useful things, but with that said, I find it visually distracting to push things to the top-level when I’m not sure that I’ll keep them and when their form isn’t too stable yet.

I’m sure there are other things that I’ve tried, too, but these are the most “solid” solutions that I’ve hit upon. Neither is particularly good.

What also prompts me to write is the fact that I’ve been working in F#, where shadowing is allowed and this issue doesn’t exist at all. Why? Because one presses F12 (“go to definition”), and warps to the point where the identifier is defined. This takes less than half a second and puts you straight into the context of the definition too, which is incredibly useful. So the “tricky shadowing bug” that spurred @evancz to outlaw shadowing entirely can be addressed by having better editor support! That makes me wonder whether this should even be a language-level issue. At most, at this point in my experience, I would say that it should be a linter warning.

Speaking of the shadowing bug, I’ve discovered just how easy it is for one to write prop instead of prop_, especially when the two mean almost the same thing. But that’s a story for another day…

In any event, let’s have a discussion. Is anyone else running into this in their day-to-day? What are your solutions? I’m sure that the question of “should shadowing be allowed?” has been explored by @evancz at some other point in time and these issues have probably been raised, so let’s try to keep it to how to practically solve this pain point. What works for you?

5 Likes

For me the argument is justified, and I also sometimes tend to give bad names to variables (well they don’t vary, but that’s what I got used to call them).

Whenever I do that it becomes harder for me to grasp the purpose and actions being done in a function. When I first started coding in Elm, whenever I had hard to find bugs it always was that I couldn’t express my intent in variable names, thus the functions got confusing and had weird cases. Now I sometimes still do it, however whenever I get the clearer picture I always rename stuff accordingly.

For me there has always been a meaningful name for different variables I am creating, since if that name wouldn’t exist, what is the justification of having the variable in the first place?

If you want to create an argument for shadowing, maybe show a SSCCE on how shadowing can solve a specific problem that the general naming practices or names specific to DSL can not? or links? It would help everyone to understand your PoV and reasoning.

So about the prefixes and suffixes I also use them but I believe they deliver some meaning in code so I don’t use them randomly. I use _ to indicate matching property in a recursive helper function, new is usually in the update function where I match the current prop of record to the new one (like {model | something = newSomething}), the indicates singleton or doesn’t mean anything for me so I avoid using it, a is a prefix I never used but maybe use if I need to indicate something is random, cur is the alias for the current, I use it to indicate I am on a base value (especially helpful if filtering list based on a selected element filterElementsForNode currNode = List.filter (\element -> .node == currNode), my seems useful if you are reinventing the wheel for some reason (tutorials, benchmarks, etc…) but never used personally. this looks like it can be replaced by curr but sounds so C++y for me.

My problem isn’t about shadowing per se: it’s about how to retain some flow with in-progress code, which is easier to write with shadowing. Perhaps you also have this problem, because I see that you write:

Of course, during the actual process, the clear picture may not have emerged yet. During the finalization of code, I also go through and rename & reorganize things properly. There’s no SSCCE that I can produce because the response to any SSCCE would be “But wouldn’t it be better with these names?”, and that response would be both appropriate and correct. It would also be unhelpful: the issue is that the “30 seconds” that I take to think of a “good” name takes me out of my coding process, and that affects my productivity. What solutions have you found to that problem?

I can see your point about the 30 sec thinking-about-a-name and how it breaks the flow. When I encounter that I just name the variable something pointless like val1, val2 to avoid having to break flow. When I go through the code afterwards to clean it up I might throw away var2 anyway, so it’s good I didn’t spend time on thinking about names.

I usually name the first thing that it might represent (like something -> somethingGetter, mappedSomething, movedSomething). The idea is if you have a new variable that is not the same thing as the old variable, you must have applied some action on the original value in the let…in statement, therefore I name according to the action I did on the previous variable. One might argue that this will make a hell of a mess after 1-2 iterations (mappedFilteredSomethingGetter(!) barely conveys any value), in that case I look at a way to use (or invent) a DSL of the app (or component). I have never have experienced long pauses to name stuff while coding with these guides, but probably would fall back to something1 case as mentioned above if the function turns out to be more complex.

To be honest IMHO if you have too many of the mentioned variables in one function, I think you have to think about how to split the thing up to more functions beforehand. For the edge cases and temporary values I think shadowing wouldn’t be helpful to the main problem, you shouldn’t have functions named something1 val1 in your code anyway. Values don’t create actions and functions don’t have data therefore having a function named same thing as a variable doesn’t make sense to me anyway.

2 Likes

I wonder if this no-shadowing rule could be moved to only operate in --optimize mode? Similarly how partial functions are OK when in the middle of building something, but not OK for shipping code?

3 Likes

How we write code is a fascinating topic to me! :star_struck:

I almost never feel the need to create shadowed variables when writing code. Your original post has made me wonder if my style or process of writing code might be the cause :thinking:

I rarely use let in my code which probably contributes a lot to me not running into shadowing issues. That’s not really a satisfactory answer though, since it just pushes the problem one level up. How do I code with minimal let while still maintaining a coding flow?

Here are three practices I use that I think contribute to this:

  1. Type-driven development
  2. Incremental approach
  3. Describing changes in variable names

Type-driven development

I’ve gotten pretty comfortable with types and now they are an important part of my problem-solving process. I’ll often play around a lot with just a signature without writing a body to explore a solution. Because of this, extracting a function doesn’t break my flow but instead helps to focus it.

If I’m dealing with a particularly complex implementation or one with a lot of moving parts, defining the signatures of those parts helps me better understand the problem and what I’m trying to do. Otherwise, it’s easy to go in circles inside my head :sweat_smile:

Incremental development

I usually try to stay in a compiling state as much as possible. This means that I try to make the smallest possible change that will get me to a compiling state. Often this is as simple as an inline no-op value. The next smallest step might be extracting that no-op to a standalone function with a signature. Then I might actually start playing with the implementation.

This process helps me stay focused. It also builds in extracting functions into the process so it happens naturally.

I wrote an article about how I use this incremental approach to implement HTTP requests. Dillon Kearns (of elm-graphql fame) also has a great article on this iterative approach.

Describing changes in a variable name

When I do use let in situations where I feel the need to shadow, I try to avoid names like myVar_ and instead try to describe how the new one is different with a suffix or prefix, similar to what @ozyinc described. This helps me understand what I’m doing in the moment as well as later when I come back and try to clean things up.

9 Likes

Just wanted to say that I’ve also arrived at this pattern of development over the years, and it’s worked wonders for me. My current employer also pushes the idea of small incremental changes.

I do the same! I make gratuitous use of Debug.todo to get everything type checking and then go to the implementation.

If I’m doing a larger refactor I will start at the top of the tree so to speak and then work my way down, I often find a better API that way than going from the bottom up.

3 Likes

I like it very much that shadowing is not allowed. It makes the code very explicit.

I agree that disallowing shadowing has made things awkward.

From the list of prefixes you suggested, I would drop the less meaningful ones:

`_` ,  `the` ,  `a` ,  `my` ,  `this`

and try to work with ones that might have some meaning in the context such as:

`new` , `cur` ,  `current`
`prev` , `previous` , `next`
`inner` , `accum`
`filtered` , `summed` , ... any appropriate verb in the past tense.

I think inner is a good one for parameters. But of course if the function is moved up to top level its parameters are not inner any more, so at that point you might rename them to remove the inner prefix.

Renaming is a little dangerous in case you miss something and that happens to match some other parameter/value with the same type and the compiler blindly accepts the bug - this is the danger with banning shadowing; it removed one bug but made another more likely. This sometimes catches me out when I forget to put inner in front of a parameter name, and the compiler flags it as a shadow, then I renamed it to put the inner on it, but miss one of the parameter instances inside the function body.

Ones like next, cur, prev are more appropriate for holding values than function parameters - as are the past tense verbs because they more denote the return values of functions that the inputs to them.

someFun props = 
    let
        nextProp = getNextProp props

        someInnerFun innerProps = ...
    in
        ...

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.