rhombus-prototype icon indicating copy to clipboard operation
rhombus-prototype copied to clipboard

Naming conventions: avoid long names

Open Metaxal opened this issue 3 years ago • 15 comments

I really understand the need for readability, but I find that long names just make source code less readable. There are just too many characters on my screen, even though they convey only limited meaning.

I'm not really talking about ridiculously long names such as as can be found in Syntax or in DrRacket's source: the usual racket user is rarely going to use them. I'm talking about more mundane names, like define and parameterize for example.

And to be clear, by no means am I advocating for one-character identifiers. I like that identifiers have explicit meanings. But for example conventions can keep the explicit part while shortening them.

I'm advocating for "if a identifier or a part of an identifier is used frequently, it should be short", with the intent to ease both readability and writability.

[edit:] What convention for name shortening we use does not matter too much to me---as long as it's consistent and intuitive enough. What I care most about is the principle above.

For example, since define is so omnipresent (I believe it is by far the most used identifier), it's already too long for my taste (and has been renamed def in different places by various racket–and likely scheme–users). I also find define-values to be awfully long and take visual space for almost nothing; defs would have been just fine.

The current- convention for parameters adds 8 characters, where some single or doubled special symbol would do the job just as well while improving the ratio meaning/character. This is so much that many core-racket parameters don't even follow this convention (see below). Even parameterize is also rather long.

Let's say, really just for the sake of the example, that parameters start in &. Then we could rewrite this

(parameterize ([parallel-lock-client lock-client]
               [compile-context-preservation-enabled (member 'disable-inlining options )]
               [manager-trace-handler (if (member 'very-verbose options)
                                        (lambda (p) (printf "  ~a\n" p))
                                        (manager-trace-handler))]
               [current-namespace (make-base-empty-namespace)]
               [current-directory (if (memq 'set-directory options)
                                    dir
                                    (current-directory))]
               [current-compile-target-machine (if (memq 'compile-any options)
                                                 #f
                                                 (current-compile-target-machine))]
               [managed-recompile-only (if (memq 'recompile-only options)
                                         #t
                                         (managed-recompile-only))]
               [current-load-relative-directory dir]
               [current-input-port (open-input-string "")]
               [current-output-port out-str-port]
               [current-error-port err-str-port]
               )

into this:

(let& ([&parallel-lock-client lock-client]
       [&compile-context-preservation-enabled (member 'disable-inlining options )]
       [&manager-trace-handler (if (member 'very-verbose options)
                                 (lambda (p) (printf "  ~a\n" p))
                                 (&manager-trace-handler))]
       [&namespace (make-base-empty-namespace)]
       [&directory (if (memq 'set-directory options)
                     dir
                     (&directory))]
       [&compile-target-machine (if (memq 'compile-any options)
                                  #f
                                  (&compile-target-machine))]
       [&managed-recompile-only (if (memq 'recompile-only options)
                                  #t
                                  (&managed-recompile-only))]
       [&load-relative-directory dir]
       [&input-port (open-input-string "")]
       [&output-port out-str-port]
       [&error-port err-str-port]
       )

which is significantly less verbose but communicates the intent just as well, if not better because now it's easier to follow the parameter naming convention. To me, the code looks less cluttered by useless characters.

Metaxal avatar Nov 12 '20 14:11 Metaxal

Using def for define is fine, it's common in other languages and there's no ambiguity. I disagree on defs for define-values simply because I've often defined defs as (defs (x e) ...) --> (define x e) .... But defvalues is still clear.

Gerbil Scheme has def, defvalues, and defsyntax which seems to work well.

However, I don't think let& is a good name for parameterize. The dynamic-scoping associations with parameterize make it different enough from let that I don't think the name should be associated with let at all. And I would rather have a whole word in a name than some "special character", I'd rather have current- than & for parameters.

AlexKnauth avatar Nov 13 '20 00:11 AlexKnauth

Please do not fix the problem of "name is too long" with "....so I'll abbreviate parts of it!" After someone mentally expands the abbreviation, they're left with a name of the same length as before except it took more steps to get there.

In my mind, the proper fix to define being both extremely common and somewhat wordy is to replace define with infix =.

jackfirth avatar Nov 20 '20 06:11 jackfirth

However, I don't think let& is a good name for parameterize. The dynamic-scoping associations with parameterize make it different enough from let that I don't think the name should be associated with let at all. And I would rather have a whole word in a name than some "special character", I'd rather have current- than & for parameters.

In languages with dynamic scope, let is parameterize, so I think let& is a very appropriate name.

I think & for parameters is brilliant if Rhombus will still support this character as a part of an identifier. Parameters are used a lot, even more than units @ and unit signatures ^ (and arguably classes % and interfaces <%>). It might make people who come from C confused though, so perhaps & should be the suffix rather than the prefix.

sorawee avatar Nov 20 '20 07:11 sorawee

@jackfirth Of course this kind of thread can only devolve into Opinion wars, but I still can't resist :)

After someone mentally expands the abbreviation

This would be true only for uncommon abbreviations (which goes against my principle above). For def I don't think that holds—the brain learns as it encounters more examples. In any case, I'm much more worried about visual clutter caused by long names, which sometimes makes it hard to find a relevant piece of code among all the (irrelevant) letters on the screen—and for this the brain can't adapt much.

replace define with infix =

I strongly object to = for binding. You likely know how many issues this has caused in many other languages. That's why Pascal used :=. Of course I understand the stance about mathematical equality, but binding and equality testing are still two very distinct operations. I would definitely keep = for testing. I'm not against something like <-> for binding (it's annoying to type though), or something else.

Regarding &, my whole point was really just about how visually lighter it all looks compared to all those noisy current-, and how easier it makes it to find a particular word in the visual field.

@sorawee

It might make people who come from C confused though, so perhaps & should be the suffix rather than the prefix.

I don't think that should be a good reason against prefix &. If you come from C, the first thing that you should be told is that you shouldn't make any expectation, and learn the basics first.

My reason for prefix & is that it's much cleaner in the let& form. Compare:

(let& ([&parallel-lock-client lock-client]
       [&compile-context-preservation-enabled (member 'disable-inlining options )]
       [&manager-trace-handler (if (member 'very-verbose options)
  ...

with

(let& ([parallel-lock-client& lock-client]
       [compile-context-preservation-enabled& (member 'disable-inlining options )]
       [manager-trace-handler& (if (member 'very-verbose options)
  ...

Though, ironically, the latter helps reading the separation between the parameter name and its bound value. For the wrong reason, I believe.

[Personally, I like to align let bindings to increase readability:

(let& ([&parallel-lock-client                  lock-client]
       [&compile-context-preservation-enabled  (member 'disable-inlining options)]
       [&manager-trace-handler                 (if (member 'very-verbose options)
  ...

but probably that's a different story.]

Metaxal avatar Nov 20 '20 09:11 Metaxal

I should have said more explicitly: Principle: It should be easy and fast to find information on the screen and in a file (say by scrolling).

Problem: Long names add a lot of visual noise that defeats this principle.

Proposal: Using conventions for naming can help reducing visual clutter.

Note: I'm not against long names for uncommon names, which communicate the intent well. Shortening these names may lead to loss of information.

A language convention is a kind of rule that the user must learn about the language. This is of course a burden for the user, hence conventions must be added and considered carefully.

@AlexKnauth As you can see in my very first (real) example, some members of the core team found current- so long that 4(!) of the parameters don't even stick to it. The example uses 13 parameter words, and replacing current- with & (for example) removes 91 useless characters from the screen. That's a whole line of text.

Metaxal avatar Nov 20 '20 09:11 Metaxal

Please do not fix the problem of "name is too long" with "....so I'll abbreviate parts of it!" After someone mentally expands the abbreviation, they're left with a name of the same length as before except it took more steps to get there.

I agree with this on most things, except for the very common used-everywhere-unavoidable forms like define and lambda. For those forms, I think it's worth abbreviating define to def, and abbreviating lambda to lam or fn.

However, this shouldn't apply to forms that aren't used literally everywhere, so in my opinion it's not worth it for parameterize or current-namespace.

For def I don't think that holds—the brain learns as it encounters more examples

This is a reason why def works as an abbreviation, while let& does not. People would learn def quickly because it would be literally everywhere, as in it will be impossible to look at a program without seeing it many times, used consistently. This isn't the case with parameterize or current-namespace, so I think abbreviating those will bring more confusion than clarity.

In languages with dynamic scope, let is parameterize

It's very important that Racket and Rhombus have static scope, and I think anything with dynamic-scope behavior needs to stay distanced from static scope, not brought closer.

AlexKnauth avatar Nov 20 '20 17:11 AlexKnauth

I more or less agree with what @AlexKnauth has said, with an additional note that let& is a horrible abbreviation for parameterize. I know it was just an example, but it is also a perfect example of why trying to shorten names can produce far worse confusion than the aesthetic displeasure that some long names can produce. Consider that let&s behavior is completely different from the 3 other let forms. It will throw an error if the variable is not already defined, it will cause a contract violation if the variable is not a parameter, the variables that it "binds" aren't accessed as variables but called as if they were functions! How confused and rightfully angry would a student be at the person who named parameterize as if it were let! This isn't Common Lisp. This example also completely neglects the fact that all of those parameters were likely named and created somewhere else at different points in time.

Having created (extremely) abbreviated naming conventions on a number of occasions, my primary take home is that they are great for expert users who use them all the time, but hopelessly inaccessible to new users. Conventions add a layer of complexity that obscures, and add arbitrary things that must be memorized in order to be able to start working in a language. Being forced to memorize conventions (PEMDAS anyone?) distracts from primary palaeoecological goals.

All this said, naming is hard, and if people can come up good short names that convey something of what the function is going to do, then that is a huge win (and one of the reasons that I love λ in Racket).

Still, you can't take a shortcut to good compact names by trying to truncate names or by setting up a bunch of local conventions using make-rename-transformer or rename-in -- the local conventions will break if someone tries to refactor a single piece of code and the local convention will have to come with, tainting other modules, or the user will have to expand the name manually, or worse, a different local convention will be defined in the landing place and the expansion will silently change into a different function! This is a disaster.

Finally, eventually all the good, meaningful compact names will be exhausted simply because there are fewer short names than there are long names. Hopefully rhombus doesn't need to make use of all of them (that would be a big language ...) but if you are going to a thesaurus then your users will still probably hate you, and the people that already struggle with all the short little names that vary every so slightly between lisp dialects will shake their heads in disbelief that the lispers have missed the point yet again.

tgbugs avatar Nov 20 '20 18:11 tgbugs

Consider that let&s behavior is completely different from the 3 other let forms.

This is a good point. I'm convinced that let& is not a good name.

sorawee avatar Nov 20 '20 18:11 sorawee

Also most of my qualms with parameterize are fixed by just indenting it differently:

(parameterize
    ([current-input-port ...]
     [current-output-port ...])
  (do-some-stuff)
  (do-other-stuff))

Long names are way less irritating if I don't let the length of a name affect how much indentation I have to use.

jackfirth avatar Nov 20 '20 20:11 jackfirth

I'm not sure how to make my point clearer but:

This post is not about aesthetics, it's about finding information on a page of code by visual parsing

So it's about using the visual system more efficiently.

Metaxal avatar Nov 22 '20 10:11 Metaxal

I strongly object to = for binding. You likely know how many issues this has caused in many other languages.

I'm not for or against = in Rhombus, but I just wanted to point out that it's used harmoniously (i.e., causes no problems) in languages like F# and Elixir and their ancestors.

bmitc avatar Jul 22 '21 03:07 bmitc

I should have been clearer: If there's additional syntax making = mere syntactic sugar, then I have less issues with that (doesn't mean I like it, but it's taste rather than reasons).

My only real issue with using = for assignment is when it might lead to silent bugs where an assignment is written when a test was meant.

Metaxal avatar Jul 22 '21 11:07 Metaxal

In SML, such a mistake is impossible to make.

To bind, one use val x = 1 (among other things, like fun, type, and module bindings) but this is only allowed at the top level or inside let/local. In all other uses = is a polymorphic infix operator for comparison.

That is, it is impossible to write if x = 1 and mean assignment; the if operator expects a boolean, and the equal operator produces one. Binding simply isn't syntactically possible there. And yes, val y = x = 1 binds y to true without issue. No reassignment.

This is probably similar to F#, which I know was influenced by SML.

benknoble avatar Jul 22 '21 13:07 benknoble

That's exactly how F# does it as well. There's no confusion.

Also, Metaxal first mentioned = in the context of binding and last mentioned it in the context of assignment. Assignment is done with a totally separate syntax in F#, using a left arrow <-.

In Elixir, = pattern matches in every use and binds, but the use is basically the same. There's no assignment in Elixir and == is used for comparison.

bmitc avatar Jul 22 '21 13:07 bmitc

Den tor. 12. nov. 2020 kl. 15.12 skrev Laurent Orseau < @.***>:

I'm advocating for "if a identifier or a part of an identifier is used frequently, it should be short", with the intent to ease both readability and writability.

I couldn't agree more. Especially declaration/definition/binding constructs benefit from being short.

I propose:

def for define defv for define-values defm for match-define

Alternatively, for defv and defm, the assignment operator could support general lvalues (both multiple values and patterns), as in:

{x y)    :=  values(42 43)
[a,b,c] := vector(44 45 46)

For parameters, I prefer a par: prefix in the same style as exn: is used now for exceptions.

Has anyone used the source code on pkgs.planet-lang.org to compute frequencies of identifiers?

Back in the day one could see frequencies at PLT Source Search: http://www.scheme.dk/blog/archive/2006_09_01_archive.html

/Jens Axel

soegaard avatar Jul 22 '21 13:07 soegaard