Friction between record destructuring, accessor functions, and 0.19's prohibition on rebinding names

In the process of upgrading an Elm app to 0.19, I’ve hit the following snag. I have a module that looks something like this:

module Widget exposing (Widget, index, toString)

type alias Widget = { index: Int, name: String }

index : Widget -> Int
index = .index

toString : Widget -> String
toString { index, name } = "Widget " ++ name ++ " number " ++ String.fromInt index

The issue is that the record destructuring in the last line rebinds the name index, and so is disallowed by the compiler in 0.19. The choices to avoid this are:

  1. rename the accessor function index
  2. rename the fields of the record
  3. don’t use a record type but rather type Widget = Widget String Int
  4. don’t use record destructuring, but rather:
toString widget =
    let
        nam = widget.name
        idx = widget.index
    in 
        "Widget " ++ nam ++ " number " ++ String.fromInt idx

However, the original code feels to me like something that ought to work. I can’t decide if this is because

  1. rebinding top-level module names ought to be allowed (but not rebinding names in nested scopes within a single toplevel definition, which I agree it is sensible to disallow)
  2. there should be some kind of exception to the no-rebinding rule for record destructuring, since the names used there must correspond to the names in the record and cannot be freely chosen by the programmer (imagine the case where the Widget type was not under my control but was imported from some other library I used)
  3. my intuitions are mistaken (always a possibility); the structure of the code sample above needs to be changed in one of the ways I listed (or one that I did not think of)

In any event, I thought I would make a post to see what others thought about the situation.

3 Likes

One other option that I think beats the others

toString widget =
    "Widget " ++ widget.name ++ " number " ++ String.fromInt widget.index
4 Likes

That’s indeed a better alternative to the code I wrote under solution 4. Thanks for spotting it! I think the general question still remains though: is this a situation in which record destructuring should be avoided?

I think the consistent solution would be to remove record destructuring from the language. I’ve yet to run across a situation where referencing record fields explicitly as in @hpate’s reply isn’t clearer.

Please, no. Pattern matching for function and let args is something I use every day. I would much rather return to the norm in languages with lexically bound identifiers, shadowing being normal and expected. I even like Elixir’s rebinding of the same variable more than once in a single let. I’d rather say model 10 times, knowing that each one intentionally shadows the one in its scope, than model2, model3, etc., and then get confused which one to reference and miss some state.

7 Likes

Not being able to shadow top-level names suggests that accessor function names might be better if they are more ‘verbified’:

toIndex : Widget -> Int
getIndex : Widget -> Int

Those names are less likely to clash with record fields which are more likely to be nouns.

Unless you start having functions in records, of course…

1 Like

My (albeit not extensive) understanding is that in a declarative functional language like elm, it is better practice to avoid naming with (procedural) verbs lite get and to. I think there is a genuine blurring of the lines between what records might hold and functions that access them which does making non-conflicting naming more challenging.

5 Likes

Another observation is that if you expose the Widget alias, it is obvious to any consumer of it that .index will extract that field, so does it really need a top-level accessor function?

Well, in the underlying code index is a lens (from the monocle library), so it’s not exactly equivalent to an accessor function.

But more generally a getter function like Widget.index is for use by other modules when accessing a widget’s index. In that way, if the representation of widget indexes changes in the future, code that manipulates widgets and their indices won’t break; I just have to make the corresponding change to Widget.index. Using .index record accessors would mean that a change in representation would need to be matched by changes everywhere the code does myWidget.index (across many modules).

Ok. But if you were doing that, you would not expose the Widget type in the module?

Correct. You’d expose the Widget type, but it would be an opaque custom type:

module Widget exposing (Widget, getIndex, toString)

type Widget =
    Widget { index: Int, name: String }

getIndex : Widget -> Int
getIndex (Widget widget) =
    widget.index
1 Like

It’s not just destructuring though, its documenting which fields of Widget are actually required by the toString function. Should Widget later grow to be more complicated, we can see at a glance that toString only actually uses the index and name fields. I think being able to reason like this about complicated code when trying to track down problems is a huge gain, and would be very sorry to lose the ability to pattern match like this.

(Of course I’m not saying any of that is required in a simple case like that.)

6 Likes

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