Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The Syntax Bikeshedding Dojo, round 5: Let bindings #218

Closed
yannham opened this issue Nov 23, 2020 · 9 comments
Closed

The Syntax Bikeshedding Dojo, round 5: Let bindings #218

yannham opened this issue Nov 23, 2020 · 9 comments

Comments

@yannham
Copy link
Member

yannham commented Nov 23, 2020

Following #207, the next item to debate is let-bindings.

Context

In Nickel, let-binding is the fundamental way of assigning a value to an identifier. It is also the preferred place to annotate terms with types. Lastly, #81 will add pattern matching capabilities to let-bindings. Currently, it supports an ML-like syntax let var = bound_exp in exp, which is widely used in functional languages (with variations): Ocaml, Haskell, Nix, Dhall, Purescript, Rust, Typescript, etc.

Some have changed the in delimiter for a ; or something else. Some use newline characters or indentation to get rid of the delimiter totally (TypeScript or Haskell for example).

Multiple let-bindings

A good share of languages also has a way of binding multiple values at once, usually for two reasons:

  • cleaner blocks of definition
  • allow mutually recursive let-bindings

That could be a nice addition to Nickel, with or without allowing recursion.

Examples:

Haskell (indentation sensitive)

let x = 1
    y = 2
    in x + y

OCaml

let x = 1
and y = 2
in x + y

Elm (indentation sensitive)

let
  x = 1
  y = 2
in x + y

Nix

let
  x = 1;
  y = 2;
in x + y

Dhall

let x = 1
let y = 2 in
x + y

Top-level let-bindings

Some languages allow top-level definitions, usually to define modules/libraries/interfaces. I don't know if that would be really useful in Nickel, since we can already define a top-level record for this purpose, which supports meta-values, recursion, and so on.

@aspiwack
Copy link
Member

Another possibility is

let <a record> in

It would behave exactly like a record, except that in would put all the record field names in scope.

@thufschmitt
Copy link
Contributor

Another possibility is

let <a record> in

It would behave exactly like a record, except that in would put all the record field names in scope.

FWIW, this is (afaik) kind-of the way Nix bindings were initially designed − iirc the very first syntax wath let <a record> which was syntactic sugar for <a record>.result. Then this evolved into the current feature, which still behaves such that let <bindings> in <expr> is exactly equivalent (I think) to with rec { <bindings> }; <expr>.

That does sound like a good idea given that let-bindings and records have very similar semantics − and from a practical point-of-view, it happened to me several times to rewrite a record into let-bindings (or vice-versa), and having the inner part syntactically identical for both made this quite easy.

@mboes
Copy link
Member

mboes commented Nov 29, 2020

My hunch is that the OCaml design should be sufficient. I find it to work better than the Haskell design, which I think was optimized for the recursive let case, except that recursive lets are not the common case in practice.

@mboes
Copy link
Member

mboes commented Nov 29, 2020

I would expect the style in the OCaml syntax to be:

let x = 1 in
let y = 2 in
x + y

@n87
Copy link

n87 commented Dec 12, 2020

TIL that Dhall allows multiple lets with single in in the end!

If Nickel doesn't want to be indentation sensitive I'd suggest either Dhall style, or

let x = 1;
let y = 2;
x + y

In my view

let x1 = 1 in let x2 = 2 in let x3 = 3 in ... let x100 = 100 in x1 + x2 + x3 + ... + x100

is less readable than

let x1 = 1; let x2 = 2; let x3 = 3; ... let x100 = 100; x1 + x2 + x3 + ... + x100

I think it'd significantly improve readability of your examples, e.g.: https://github.com/tweag/nickel/blob/master/src/examples/lists-foldl.ncl (where you had to dedicate a separate line for in )

@Profpatsch
Copy link

I find the dhall syntax to be the nicest in practice, mostly because you don’t have to internally branch on a conditional whether there is already a binding in the let, and indentation is a non-issue (since everything is prefixed by a let)

let a = 
let b =

vs

let a = 

oh wait now I add a b and I have to reflow the block for better indentation (otherwise you get a strange overhang)

let
  a = 
  b =

I’ve come to do the same in Haskell within do blocks, just always prefix with let, less mental overhead.

The in before another let is not necessary btw, because you can instruct the parser to end the block when it sees the next let.

@Profpatsch
Copy link

The let … in syntax has the additional problem of making it not clear whether you want to put the in at the end of the line or the beginning of the next, plus it makes refactoring annoying because you always have to add or remove stray ins.

@settings settings bot removed the bikeshedding label May 31, 2021
@akavel
Copy link
Contributor

akavel commented Sep 15, 2021

Given that this thread is explicitly marked as bikeshedding dojo: could it be possible to have a syntax that allows "top-down" code writing, not only "bottom-up"? What I mean, is to allow for example something like:

config-gcc.ncl:

{
  flags = ["Wextra", {flag = "o", arg = "stuff.o"}],
  optimizationLevel = 2,
} | #Contract

where Contract = {
  pathLibC | doc "Path to libc."
           | #Path
           | #SharedObjectFile
           | default = "/lib/x86_64-linux-gnu/libc.so",

  flags | doc "
            Additional flags to pass to GCC. Either provide a string without the
            leading `-`, or a structured value `{flag : Str, arg: Str}`.
          "
        | List #GccFlag
        | default = [],

  optimizationLevel | doc "
                       Optimization level. Possible values:

                        - *0*: unoptimized
                        - *1*: normal
                        - *2*: use optimizations
                      "
                    | #OptLevel
                    | default = 1,
}

where OptLevel = ...

instead of the person reading the code having first to laboriously track down "the main final object" somewhere more or less near the end of the file to understand what the whole file is actually about.

edit: In somewhat different words, if what I wrote above is possibly not super clear: there are programming languages which require the code author to define every entity before it's referenced (incl.: Nix, Pascal, OCaml, C & C++ [with the workaround of empty declarations]), and there are also programming languages which allow entities to be defined in an order chosen by the code author, possibly after they're first referenced (incl.: Go, Rust). Notably, AFAIK the requirement of "define before referenced" originally was basically due to resource limitations of early computers, which forced compilers to be "one-pass", and thus forced language designers to make languages one-pass-compatible. My personal experience is, that it's noticeably easier to read source code when it was written in a "reverse" order of dependencies. In fact, what's crucially important to me, is that it's noticeably easier to not read the source code in such case: that is, to quickly notice that I don't need to read this file at all, because it concerns with something that's not important to me at this moment. That's typically the first thing I try to evaluate when I read any fragment of any codebase - to understand if it's even relevant to a task at hand that I'm trying to perform in the codebase (be it debugging, adding a new feature, or refactoring). Secondly, seeing a "high level outline" of the file at first gives me a "birds eye map" of what happens in the file, and helps me then "navigate" to specific sub-features (dependencies) that I want/need to explore further - while ignoring the ones I don't need. Kinda like having a "Table of Contents" at the beginning of a reference manual, or how "menus" are structured in GUIs.

To make it clear: with regards to this project, I'm a Nobody From The Internet™, and I completely understand and respect that, and appreciate any decision that will eventually be undertaken by the awesome authors of this really cool project. I just wanted to try and make sure - feeling encouraged by the explicit brainstorming/bikeshedding note here - that this thought is at least given a look as part of the design/bikeshedding phase, that it's not purely accidentally overlooked before it's too late to even think of raising it. (Though, the Nim language kinda shows there might sometimes still be chance to try and do it even later; although it also shows it's quite more difficult if possible at all.)

@yannham
Copy link
Member Author

yannham commented Dec 29, 2021

Superseded by #494.

@yannham yannham closed this as completed Dec 29, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants