reasonml / reason

Simple, fast & type safe code that leverages the JavaScript & OCaml ecosystems
http://reasonml.github.io
MIT License
10.13k stars 428 forks source link

Attempt to remove semicolon #1613

Closed chenglou closed 1 month ago

chenglou commented 6 years ago

We can maybe do it when it's not ambiguous, but to do it for all cases is tricky.

let x = something;
(printString);
(anotherThing);

Can't remove the semicolons.

let x = something;
let y = anotherThing;

Can remove the semis.

[@floating attribute];
let x = foo;

Can't Remove the semis.

imperativeCall(foo);
(computeCallback())(bar);

Can't remove the semis.

jordwalke commented 6 years ago

There's one consequence about removing the semis, even in the case where it's not too difficult:

If we remove these semis

let x = something;
let y = anotherThing;

We get:

let x = something
let y = anotherThing

And then when people add another line after the let y line, they sometimes have to go put the semi back before adding their next line:

let x = something
let y = anotherThing;
(234 + 234) |> something

In fact we deal with this issue today because Reason does remove the final semi in a { } sequence (it's the one time Reason currently does remove the semi), and then people get bit because they forget to add it back again when adding a subsequent line. People have requested that we add back the final semi for this reason.

jaredly commented 6 years ago

There's a difference between "ASI" that javascript does (which is all kinds of terrible), and "treating a newline as significant" (which swift does).

In swift, an opening brace for a function call is not allowed to be separated by a function call. So

x
(1,2)

means x; (1,2) I think this would be fairly simple to implement and would remove nearly all the need for semicolons.

As for the @decorator, would it be so terrible to go back to @@decorator for a "floating standalone decorator"?

strega-nil commented 6 years ago

Why do we want to remove semicolons? I really dislike removing semicolons :( The solutions are always subpar (whether ASI, OCaml-style, or go-style). IMO, the only real solution is to remove semicolons almost entirely, and have newlines be significant, like in python.

(otoh)

that would break code like

let x =
  y;

you could have it not break statements after binary operators, which does break my preferred style of

x
  + y
  + z

and forces you to write

x +
  y +
  z
jordwalke commented 6 years ago

It's worth using this thread to catalog the various solutions that other languages do - continuing off of @jaredly's example of Swift.

Risto-Stevcev commented 6 years ago

Semicolons introduce a lot of unnecessary visual noise, most people really like the python/purescript/haskell syntax as being more readable

I've never had any issues with automatic semicolon insertion in javsacript. It's very easy to remember the rules where you don't need them: https://docs.npmjs.com/misc/coding-style#semicolons. It's like the fat arrows in JS -- it seems like a small thing but it really makes code more readable than having all the extra noise

I think whitespace sensitive languages are nicer though because there's a lot more discipline involved with how code is formatting, and because the layout is consistent across a lot of code it's easier to scan the codebase. I've seen a lot of people complain about the semicolons in reasonml when it gets brought up

jaredly commented 6 years ago

@ubsan it wouldn't break that code! (see swift, where all of your examples work!)

let x =
2
+ 3

is perfectly valid swift! (and x resolves to 5)

strega-nil commented 6 years ago

@jaredly I can't actually figure out what swift does, it doesn't seem to be documented anywhere? Is there documentation somewhere that I'm missing?

@Risto-Stevcev that's not well supported. Those language communities do prefer not having semicolons, but that's not universal; semicolon-having language communities often prefer semicolons (like me, I like semicolons) (also just because you were never confused by ASI doesn't mean people haven't been confused by ASI)

Anyways, if we were to go the semicolon free route, I'd prefer to go the swift or python route, where whitespace is significant, but I would be unhappy with that solution, as I like semicolons.

mrkaspa commented 6 years ago

I like how the semicolons are used in rust, they are only used to separate expressions inside the functions or block of codes, but reasonml overuses this.

like in a type declaration

type retainedProps = {message: string};

like in module declarations

module SumMonoid = {
  type t = sum;
  let mempty = Sum(0);
  let mappend = (Sum(a), Sum(b)) => Sum(a + b);
};

like in functions declared in a block at the end of the {}

jordwalke commented 6 years ago

@mrkaspa It's definitely possible to have reason parse and print fewer semicolons that it currently does. However, when mixing in imperative statements it gets really difficult to know when you can omit them, especially when simply adding a new statement at the end of a sequence - you often have to go back and add a semicolon to the end of an unrelated line which feels kind of high friction to me. I'll have to look into what Swift does to see if something like that would work. There is something very nice about striving for being 100% agnostic to newlines if you can achieve it.

I'd really like to not have to go back to @@decorator for floating decorators though. Two kinds are better than three, but it's still much better to have one kind.

We might take this one a little slower, by doing the following:

  1. Accepting, while not requiring, and not printing semicolons in a couple of places that don't result in any "whoops, I forgot a semicolon on the previous line" situations. For example, we could accept and print this (though you are free to add more semis):
module X = {}
module type X = { }
let foo = bar;

Another place where we could trim the semis is after imperative if/else/for/while:

if (something) {
  foo;
} else {
  bar;
}        /* Look no semi */
while (anotherThing) {
    doIt();
}        /* Look no semi */
let foo = bar;

Those if/while cases are places where semis aren't needed in ES6 and where they wouldn't be required in Reason because the parser knows to expect some braces which conclude the construct.

I'm pretty sure that small step forward would be a win for many people, a loss for no one, and doesn't mean we can't remove even more later, in other cases that are also unambiguous wins.

strega-nil commented 6 years ago

@jordwalke I would rather look to Rust, than Swift. For one, it's far better documented; for two, I like semicolons and I think Rust does semicolon-removal really well (it only removes semicolons where you'd expect them to not be there). Basically, it's block-based - if you have a block in "statement position", there's a semicolon inserted, otherwise no.

fn main() {
  {
    ...
  } // no semicolon needed
  let x = {
    ...
  }; // semicolon needed
  ({ ... }); // semicolon needed
  {
    ...
  } // this is the last statement in the block, so it's used as the return expression
} // no semicolon needed here either

This could look like

let f = (x, y) => z;
let f = (x, y) => { z } // could go either way
let f = { z } // could go either way
let f = (x, y) => {
  { z } // no semi needed
  ({z}); // semi needed
  let z = { u }; // semi needed
  0
};
cullophid commented 6 years ago

could you not just force the parens to be on the same line as the function

someFunc(
 42
) // valid

someFunc
  ( 42 ) // invalid?

I would really like to see semis go away, but I think the answer is ether to remove them entirely, or keep all of them.

cullophid commented 6 years ago

It seems to me that the solution to removing semis is to make linebreaks significant (in some situations)

jordwalke commented 6 years ago

@cullophid

It seems to me that the solution to removing semis is to make linebreaks significant (in some situations)

I think it has to be that way, sadly.

could you not just force the parens to be on the same line as the function

Yes, Jared mentioned that this is how Swift does it.

cullophid commented 6 years ago

@jordwalke I dont think significant whitespace/linebreaks is a problem... Its an issue in js because it limits minification, but in pretty much any other case it makes a lot of sense to me.

mrkaspa commented 6 years ago

In reason everything seems to have semicolons why not to remove them?

mrkaspa commented 6 years ago

Found this article http://sigkill.dk/writings/semicolons.html why not to use auto semicolon insertion?

chenglou commented 6 years ago

We already do

mrkaspa commented 6 years ago

I meant that Scala uses automatic insertion at line breaks and Go uses automatic insertion. You do not insert the semicolon in any of those languages, perhaps it is inserted before compilation

Risto-Stevcev commented 6 years ago

@chenglou Love the auto semi insertion, is there a way to configure refmt to turn off automatically inserting them?

chenglou commented 6 years ago

Not currently, as it's still uncertain whether we can pull off removing semis completely, so we're printing them back for future-proofing, for now.

@mrkaspa see @Risto-Stevcev's reply

mrkaspa commented 5 years ago

Any progress on this?

IwanKaramazow commented 5 years ago

https://reasonml.github.io/try?rrjsx=true&reason=DYUwLgBAHhC8EG8BQEKkgQzhAjCt4EARtgEz5YDUxSAvkA In most non-ambiguous cases semicolons aren't required any more.

mrkaspa commented 5 years ago

@IwanKaramazow looks cool how can I turn this on in refmt? because always put the semicolon

jordwalke commented 5 years ago

We aren't willing to commit to being able to always support omitting semicolons in all future syntactic changes, so we offer relaxed semicolons as a dev-time enhancement, that isn't guaranteed to always remain so. It's also possible we could guarantee it one day.

yunti commented 4 years ago

Is this possible now that the new bucklescript syntax has removed it?

texastoland commented 11 months ago

Ping 🛎

This is purely easthetic but arguably key for adoption ("Syntax Matters"). It seems increasingly conventional in OCaml and TypeScript, the default in ReScript (below), and prevalent in MLs like F# or Haskell variants.

https://github.com/rescript-lang/rescript-compiler/blob/master/jscomp/syntax/src/res_printer.ml