Skip to content
This repository has been archived by the owner on Jun 15, 2023. It is now read-only.

Spread anywhere feature #692

Merged
merged 18 commits into from
Oct 25, 2022

Conversation

butterunderflow
Copy link
Contributor

Feature request from #670.
The new feature will allow spread anywhere in a list creation expression.
For example, list creation like

let _ = list{1, 2, 3, ...x, 4, 5, ...y, 6}

will no longer trigger an error anymore, but parsed to

let _ = Belt.List.concatMany [|(1 :: 2 :: 3 :: x);(4 :: 5 :: y);[6]|]

Can anyone help review this feature PR? Thanks ^v^

@butterunderflow butterunderflow marked this pull request as ready for review October 20, 2022 09:12
@cristianoc
Copy link
Contributor

Would you add printing tests?

@butterunderflow
Copy link
Contributor Author

What is printing tests stand for? Like printing some parse results related to this feature?

@cristianoc
Copy link
Contributor

Add a file in tests/print or extend an existing one where list examples are.

It will tell you how things print, and whether printing is idempotent, so you don't get different results if you format twice.

@butterunderflow
Copy link
Contributor Author

Thanks, I think I understand. I'll give it a try.

@cristianoc
Copy link
Contributor

See how the syntax of normal lists is preserved by printing.
A similar mechanism is needed for this extension to lists.

@namenu
Copy link
Contributor

namenu commented Oct 20, 2022

tiny concern, but would it be better not allowing this expression at all ?

lists are not okay when manipulation is occurred at non-head position.

Once spreadings are used inadvertently, it would cause performance problem.

By now, list spreading guarantees constant time

@butterunderflow
Copy link
Contributor Author

IMHO, it's still a useful feature. For some reason.
First, IMHO again, list concatenation is a little bit verbose at present. We have to write some prefixes of Module.

Belt.List.concatMany([list{1, 2, ...x}, list{3, ...x}, list{4, ...x}, list{5}])
// or 
Belt.List.concat(list{1, 2, ...x}, list{3, ...x})
// or 
List.concat(list{list{1, 2, ...x}, list{3, ...x}})

Second, I think if we support @ operator (like OCaml), the verbose issues will be moderated. But for the existence of list open token list{, looks still not concise enough. Like we have to tell the reader repeatedly "it's a list".

// pretend we have operator @ 
list{1, 2, ...x} @ list{3, ...x} @ list{4, ...x} @ list{5}
list{1,2, ...x} @ list{3, ...x}

If we let spread show anywhere, we can just write:

list{1, 2, ...x, 3, ...x, 4, ...x, 5}
list{1, 2, ...x, 3, ...x}

I think that's more concise, right?

And, as far as I know, literally, the word "spread" itself is not intuitively associated with constant time operation, unlike "prepend".

Finally, the performance issue you mentioned does exist while this feature is being misused. But this feature will not downside any performance of existing code, and I think (I'm not sure) concatenate seems can be optimized in some way. So I think it's still a good deal. I'll always wait for more suggestions.

@IwanKaramazow
Copy link
Contributor

IwanKaramazow commented Oct 21, 2022

Before adding this to the language, we should take a closer look at the design of this feature.
What is the reach of this problem? There are 3 vectors:

  • volume of GH issues, forum posts, twitter etc…
  • how blocking is this problem if the developer encounters it
  • how many of the total ReScript devs does it affect in their day to day work?

When making design decisions regarding new syntax in the language, it’s also important to weigh the costs, not all of which are performance, against the potential benefits. The following things can happen:

  • The language gets larger.
  • The language gets slower.
  • The documentation becomes more extensive.
  • We're spending time developing new syntax, rather than refining other parts of the language.
  • We run the risk of introducing changes that don't play well with existing syntax. (Why don't we allow this for array then?)

I feel like this more design work needed before going into implementation mode.

@bobzhang
Copy link
Member

bobzhang commented Oct 21, 2022

Let me explain a bit why this feature it is useful and why you should not worry about performance.

This feature is very helpful for expressing lots of algorithms, for example

Add(e1,e2) => list {... compile(e1), ... compile(e2), Add }

It is much more readable than the alternatives, and as efficient as you could, in theory the compiler can do
a better job than you since it kows the compile is a temporay variable and can do some smart mutations.

So the argumets are:

  • You should avoid list if you care about performance
  • If you want to concate two lists, the alternative is not better at performance, you have to express this pattern. Simply disabling this syntax does not mean you can avoid the problem, it does not give a solution. JavaScript is also more consistent here

Before we add this feature, this used to be a bug, the syntax was accepted but not reported error, it is worse so
that we are not enriching the syntax: we are fixing existing issues.

With regard to developer resources, this is the feature I would like to add on day one when making BuckleScript,
finally, I have some dedicated resources to make it possible, it also makes the sample code for PL course in ReScript much more readable, instructions concatenation are everywhere.

Hope my explanantions make the motivation clear.

Edit: note this implementation should be improved, @butterunderflow no regressions should happen, x::y should be interpreted as it is, only some invalid patterns are now accepted -- it was silently ignored, so this PR should be a strict better improvement

@@ -114,7 +100,7 @@ let arr = [|x;y|]
let [|arr;_|] = [|1;2;3|]
let record = { x with y }
let { x; y } = myRecord
let myList = x :: y
let myList = Belt.List.concatMany [|x;y|]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • this case should be specialized?
  • this case too { ... x}

@@ -2,6 +2,7 @@ let x = list{}
let x = list{1}
let x = list{1, 2}
let x = list{1, 2, 3}
let x = Belt.List.concatMany([list{1, 2, ...x}, list{3, ...x}])

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this be improved? print it as list{ 1, 2, ...x , 3 , ...x}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be, but just by changing the printer can cause some problems. For example, list{. . x, . .y} and Belt.List.concatMany([x, y]) will be printed as the same thing. I am still trying to solve this problem.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add an attribute to Belt.List.concatMany to avoid accidental simplifications?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what is done for string templates, I believe, to differentiate from normal string concatenation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check what isTemplateLiteral does.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for all your suggestions, I'll learn more about that.

@butterunderflow
Copy link
Contributor Author

Made some changes to ensure that the printer output and source are the same. I would have to ask you to review it again, thanks!

@IwanKaramazow
Copy link
Contributor

Thanks for the context @bobzhang 👍

src/res_core.ml Outdated
(* | (true (\* spread expression *\), expr, _) :: exprs ->
* let exprs = check_all_non_spread_exp exprs in
* makeListExpression loc exprs (Some expr)
* | exprs ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of comment unused code, can you just remove it

(Longident.Ldot (Longident.Lident "Belt", "List"), "concatMany");
}
when hasSpreadAttr expr.pexp_attributes ->
true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simplified: -> hasSpreadAttr exp.pexp_attributes

6 │
7 │ let list{...x, ...y} = myList
8 │
9 │ type t = {...a}

List pattern matches only supports one `...` spread, at the end.
Explanation: a list spread at the tail is efficient, but a spread in the middle would create new lists; out of performance concern, our pattern matching currently guarantees to never create new intermediate data.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we may need update the docs about pattern in later commits, it is not supported since it is not deterministic

@@ -2,6 +2,8 @@ let x = list{}
let x = list{1}
let x = list{1, 2}
let x = list{1, 2, 3}
let x = list{1, 2, ...x, 3, ...x}
let x = Belt.List.concatMany([list{1, 2, ...x}, [list{3, ...x}]])

let x = list{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add some more test cases, some edge cases like:

let x = list{ ...xs}
let x = list{1, ...xs}
let x = list{xs, ... ys}
let x = list{ ...xs, ... ys}

@bobzhang
Copy link
Member

@butterunderflow looks good to me once the comments are addressed.
Note Belt.List.concatMany is an implementation detail, in theory, we could swap more efficient ones in the future

@butterunderflow
Copy link
Contributor Author

Done. Please check again, thanks!

@bobzhang
Copy link
Member

@butterunderflow Thanks, cannot wait for to use it in the course!

@bobzhang bobzhang merged commit 402b9c8 into rescript-lang:master Oct 25, 2022
@cristianoc
Copy link
Contributor

@butterunderflow would you add a line to CHANGELOG.md

@butterunderflow
Copy link
Contributor Author

Done. Please check #698

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants