Reason: [proposal] using `< >` instead of `()` for type parameters

Created on 24 Nov 2017  ·  71Comments  ·  Source: reasonml/reason

Recently after reading some issues using the syntax v3, I found it's painful to see so may parens.

for example, here is a very simple type, it would be even worse for complex types

(int, Js.nullable(int)) => int

it would be much nicer to write it as

(int, Js.nullable <int>) => int

the same applies to type definitions:

type list <'a> = 
  | Nil
  | Cons of ('a * list <'a>)

The root cause is () used in too many places, I have to count the parens for complex types.
I guess it is okay to use <> in types, in expressions there may be conflicts with comparisons, in type level, should be good?

Parser RFC

Most helpful comment

I still really think that losing the parens is a mistake. Parens are for function calls, and a type ctor is a function call. It's similar in the theory, so it should be similar in the syntax:

let f(y) = z; /* value */
type f('y) = z; /* type */
module F(Y) = Z; /* module */

All 71 comments

Is the space between Js.nullable and <int> intentional? It seems weird to have them detached like that.

Also, this needs to be considered along with the tuple syntax, which suffers from the same problem. This type has three sets of parentheses with different meanings:

((int, Js.nullable(int))) => int

Is the space between Js.nullable and intentional? It seems weird to have them detached like that.

Not intentional.

Also, this needs to be considered along with the tuple syntax,

Yes, I have the same feelings, and proposed to use [ ] for tuples, but did not make it

In general I like the idea of reducing the amount of parens used as there is a lot of them in the new syntax. But also similarly to what @glennsl is saying I think that we need to do something about tuples first, because it has been a way bigger issue for me so far.

I really dislike this idea. For one, higher kinded types are functions,
so the similarity in syntax is warranted. For two, coming from C++ and
Rust, <> in the syntax for type arguments and parameters just does not
feel very good - tooling doesn't really support it, and it makes for either
infinite-lookahead or weird syntax oddities (::<..> in Rust).

On Nov 24, 2017 05:09, "SllyQ" notifications@github.com wrote:

In general I like the idea of reducing the amount of parens used as there
is a lot of them in the new syntax. But also similarly to what @glennsl
https://github.com/glennsl is saying I think that we need to do
something about tuples first, because it has been a way bigger issue for me
so far.


You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/facebook/reason/issues/1661#issuecomment-346825010,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ADUV7eFlq1SY-Kp-f-fK_t_V4wnY6C5Oks5s5r-egaJpZM4QpwWK
.

I can’t think of any technical challenges with <> for types. The conflicts in expressions wouldnt come from inequality operator because we already remap this to !=
Any conflicts in expression real estate would come from JSX if anything.
I do think it’s a fair point about there being many parents in lambda types - making it difficult to see where the lambda arguments end.

I had considered the idea of using <> to represent the type of React components (or any element constructed via jsx) so that we can print the type of nested React elements and have their types appears similar to the jsx that was used to construct them.

@ubsan ocaml does not have hkt, so your analogy does not apply. Also it is really bad to explain hkt to new users on day one

@bobzhang agh, meant type constructor. This is what very little sleep means -.-

@bobzhang also, I very much disagree with the idea that type constructors, or HKT, are confusing. All they are is functions over types.

I'm mildly against '<>', but I wouldn't be very sad. I'm just not offended by many parens :D and I can appreciate the similarities to Flow, Rust, Swift, Java.... pretty much all of the more popular typed languages.

@jaredly pretty much sums up my take on it, unless there's some insurmountable conflict I'm not seeing.

_/me is wondering why so many parenthesis at all as int * int Js.nullable -> int (or even int, Js.nullable int -> int) seems far more readable..._ o.O

/me is wondering why so many parenthesis at all as int * int Js.nullable -> int (or even int, Js.nullable int -> int) seems far more readable... o.O

I do not think that int * int Js.nullable is more readable than Js.nullable<int, int> to most programmers.

I personally find Js.nullable(int, int) slightly more readable than Js.nullable<int, int> but I can totally see why more people would find the second instantly recognizable, and therefore superior.

However, right now, I think the symmetry between values Variant(x, y) and type Variant(int, int) particularly important. That's because there's a slight nuance in distinguishing tuples vs. multiple arguments to variants, and it's good for that nuance to be consistent everywhere tuples occur.

We at least currently have a symmetry:

Variant((valOne, valTwo)) of type Variant((int, int))
Variant(valOne, valTwo) of type Variant(int, int)

If we used < >, we would no longer have this symmetry, and instead we'd have:

Variant<(valOne, valTwo)> of type Variant((int, int))
Variant<valOne, valTwo> of type Variant(int, int)

Since the extra required parens may be a surprise, I think the least we could do is be consistent about it as a way to help people remember. If we had a different syntax for tuples, then I believe < > for type parameters has a stronger case.

There’s no symmetry since OCaml does not have hkt. I created this issue not because of taste, but due to the fact that I have to count parens to read a not so complex type

Sorry but my comment about symmetry is not in reference to hkt - it's simply in reference to the tokens themselves. I do agree that the variety of <> makes some paren counting easier, and I'm conflicted because it currently presents a token asymmetry in one area of the syntax that's already kind of hard to remember the rules for.

In OCaml, there is only parameterized types which almost the same with c++ generics

val fold_right2 : ( 'a * 'b -> 'c -> 'c) -> 'a list -> 'b list -> 'c -> 'c

in reason

val fold_right2 : (((('a *'b),  'c ) => 'c ), list ('a), list ('b), 'c) => 'c

in reason with <

val fold_right2 : (  ((('a *'b), 'c) => 'c), list <'a>, list <'b>, 'c) => 'c

in reason with < for generics and [ for tuple

val fold_right2 : ( ( ['a,'b], 'c) =>'c,  list <'a>, list <'b>, 'c) => 'c

The latest still looks less readable, but seems to be a reasonable trade off

@bobzhang

let fold_right2: ((('a, 'b), 'c) => 'c, list('a), list('b), 'c) => 'c;

imo, with proper formatting, this becomes far more readable. With a separate syntax for tuples:

let fold_right2: ((['a, 'b], 'c) => 'c, list('a), list('b), 'c) => 'c;

this is quite readable! (for such a complicated type)

Bob, I believe the second example converted to Reason via:

> echo " val fold_right2 : ( 'a * 'b -> 'c -> 'c) -> 'a list -> 'b list -> 'c -> 'c" | refmt --interface=true --parse ml --print re

Should be:

let fold_right2: ((('a, 'b), 'c) => 'c, list('a), list('b), 'c) => 'c;

The first argument to fold_right2 doesn't need extra parens. (I think @ubsan points out the same thing). So, it's not quite as bad.

I don't mind the <'a> syntax either:

let fold_right2: ((('a, 'b), 'c) => 'c, list<'a>, list<'b>, 'c) => 'c;

But I actually don't think it helps in this specific case too much. I think it would help in some other cases, although tuple arguments are not super common.

I think in your example the tuples being represented as [ ] had the much bigger impact.

I wish it weren't the case that so many people think of [x, y, z] as being arbitrarily long sequences of things because in that case [ ] for tuples would clean up a lot of stuff. As it stands, people "just get" what [x, y] means in their code today. One challenge if anyone is up for it: determine if it's possible/feasible to create a Hetergeneous List data structure can function as tuples of statically known length with different types at each slot, as well as supporting appending to the head/concating etc so it can function as a homogeneous list if necessary. It's a really long shot, but in that case you kind of get the best of both worlds, and if it's possible it would be worth spending considerable effort/changes to make it happen. (Please get ahold of me if you think this is feasible before going down the rabbit hole on this one).

One other thing that would reduce the number of parens (whether or not we use <> or anything else for tuples even) is to print single identifier token arguments/types without their parens. This is valid ES6, and encouraged by JS pretty printers as well.

Instead of:

type t = ((int) => int, string);

We should print:

type t = (int => int, string);

FWIW, the new data structures introduced in BuckleScript will have types like
('key, 'value, 'id) map which is exactly the same as c++ map <key, value, less<key>>
The expressivity of OCaml parametric types are in the same level of c++ generics(maybe less), there is no type application in OCaml, parameterized types are mostly generics conceptually

Yeah, I agree that OCaml types aren't actually type application, but it does help to get people to think about it in terms of applying functions just so they know the syntax is the same as function application. But in that case, the only aim was making it easier to remember/learn type syntax, and <> should also bring that same kind of ease of learning because it's so ubiquitously used. I'm not opposed. It could also be a smooth upgrade too because there is probably space in the syntactic real estate for both (we could parse as () and print as <>).

There are two things I think the <> would have us give up. One is speculative, and not a very strong reason. The other is a stronger reason imho:

Weak reason:

We could have some type sugar for JSX elements like this:

let myTypeaheads = [<Typeahead />, <Typeahead />];
type typeaheadList = list(Typeahead.element);
// With the sugar:
type typeaheadList = list(<Typeahead />);

You could have list<<Typeahead />> but it's just ugly.

Stronger reason:
The nuance of our current tuple syntax. It's hard to remember, but at least it's consistent everywhere.

type myType('t) =
  | Variant('t);

let x : myType(int) = Variant(0);
let x : myType((int, int)) = Variant((0, 0));

Any time you see (onething) in the type you see (onething) in the value.
Any time you see ((two, things)) in the type you see ((two, things)) in the value. I don't like the (()) double parens, but I would like them even less if they were inconsistent throughout the grammar.

type myType<'t> =
  | Variant('t);
let x : myType<int> = Variant(0);
let x : myType<(int, int)> = Variant((0, 0));

If we had a better syntax for tuples (one that didn't result in an unintuitive syntax for lists), it would solve so many problems including this one.

I found out today that first-class module types need to be wrapped in parentheses too. So just to take the extreme example one step further, here's a type expression with four groups of nested parentheses all having different meanings:

((int, list((module X)))) => int

Here is @glennsl's example with <> for type parameters.

((int, list<(module X)>)) => int

It breaks it up a bit (in a good way).

I think we can also stop requiring parens for first class modules regardless of if we use <> for type parameters:

((int, list(module X))) => int

Which is about as clean.

Then combining the two is probably the cleanest looking of all:

((int, list<module X>)) => int

Can someone address my most meaningful argument against <>?
Specifically, what I perceive to be an inconsistency here?

type myType<'t> =
  | Variant('t);
let x : myType<int> = Variant(0);
let x : myType<(int, int)> = Variant((0, 0));

Can people speak to how big of an inconsistency they perceive this to be? @glennsl, @jaredly, @bobzhang ?

I don't perceive it as an inconsistency at all. I think they should look different because they are different. You could also just as well have let x : myType<(int, int)> = VariantB("foo");.

I am slightly bothered by seeing parentheses nested inside angle brackets, however: <( ... )>. Not entirely sure why. Perhaps because it looks like a slightly chubby but very stern guy 🤷‍♂️

Good to hear parentheses might not be needed for first-class modules. That's one less to worry about at least.

". Perhaps because it looks like a slightly chubby but very stern guy" 😆

Okay, well it's good to hear at least one other person doesn't perceive an inconsistency.

You know, supposedly Lisp people say that eventually the abundance of parenthesis don't bother them and their brains adapt. 🤷‍♂️

They've also invented quite a bit of tooling to help deal with them, such as rainbow highlighting and paredit.

Using parens here gives the user one piece of syntax to remember. One of the early selling points of Reason's syntax was making type parameters look like function parameters in Reason - is that worth giving up?

I really don't think so - thinking of type application as function application is a really nice idea, and is how one should think of type application in languages with stronger type systems, like C++ or Haskell. Maybe Reason doesn't have that powerful of a type system, but it's still a nice idea.

I just really, really dislike <> being used as type parameter lists. If people really wanted to change them, I'd rather we just changed them to [], to be similar to scala/python. <> just feel wrong as type parameter/argument list bounds, despite having used them for years in C++ and Rust.

I agree with @hcarty one of the nice things and one of my favorite things about reason. Adding more syntax to differentiate type parameters isn't something I'm excited about.

I do admit that the parens get kind of overwhelming. Perhaps we can start by first eliminating all the obvious superfluous parens. That could at least get us partially there.

simple identifier lambda arguments:

/* Before */
List.map((itm) => item + 1, myList);
/* After */
List.map(itm => item + 1, myList);

first class modules:

/* Before */
type t = ((int, list((module X)))) => int;
/* After */
type t = ((int, list(module X))) => int;

Can anyone think of any other obvious improvements had by merely removing parens where they are not necessary at print time?

switch:

/* Before */
switch (f(x)) { ... }

/* After */
switch f(x) { ... }

if:

/* Before */
if (isWhack(x)) { ... }

/* After */
if isWhack(x) { ... }

(the same also applies for try)

record destructuring single lambda argument: (probably ambiguous in one way or another)

/* Before */
({ x, y }) => ...

/* After */
{ x, y } => ...

lambda type annotation (I also find this much more intuitive)

/* Before */
(x: arg) => ...
x: ret => ...

/* After */
x: arg => ...
(x): ret => ...

I find tuples to be the worst offender by far, however, since I often use tuples with lambda functions and get abominations like List.filter(((x, y)) => ...), or even with ReasonReact <Foo> ...(((error, marks)) => ...) </Foo>. Unfortunately I know of no good solutions.

Edit:

Single wildcard argument in lambda function

/* Before */
(_) => ...

/* After */
_ => ...

I think for Reason React, we can do:

{((error, marks)) => ...}

Which helps a bit. I don't know if it's preserved at print time.

thinking of type application as function application is a really nice idea

It is simply wrong to make such assumption, OCaml does not have it, let's not pretend OCaml have it. Actually it is more confusing. Take Hashtbl.t ('a,'b) for example, would it be equivalent to Hashtbl.t ('a) ('b)? No!

This does not mean OCaml's type system is not expressive, OCaml has more advanced stuff compared with hkt, e.g, modules and we will have a very nice encoding in the stdlib to show you can do more advanced stuff compared with hkt while not loosing efficiency.

In my opinion, parens should be only used in two cases:

  1. function/ function application

  2. explicit group to resolve precedence issues, it is a buggy design if extra parens changes semantics.

I think list < module X> should parse without conflict

@bobzhang So you'd prefer that variant constructors used <> as well? They don't curry either. Given

type foo =
  | Mk_foo(int, int);

Mk_foo(0)(1) is also not valid. I don't think "not currying" is a valid argument against parentheses for type application - if anything, it's unexpected that function application with parentheses does curry.

@ubsan You example is also a very good example of abusing parens, Mk_foo is not a function, let's not pretend it is a function.

type foo = 
  | Mk_foo of  int , int 

if there is no parsing conflict we can unify it with GADT

type foo = 
  | Mk_foo : int, int 
type foo<'a> = 
  | Mk_foo : int, int => foo< int> 

@bobzhang not every different semantic needs to have different syntax. Even if they might not be exactly the same, giving up the similitude of function application, variant construction, and type application syntax is a mistake, and, imo, a poor syntax decision.

not every different semantic needs to have different syntax

I agree with you. The thing is the two use cases of paren (listed above) is already pervasive, we should not use it in other places any more, the code is too hard to read otherwise

I still disagree that it's hard to read. In fact, I think it's substantially easier to read than <>

I think readability is a better argument than having syntax consistently hint towards semantics. Even the original OCaml used infix commas (w parens) to represent tuples as well as multiple arguments to variant constructors.

Here is a list of possible improvements we can make to reduce the number of parens. I'll list what I personally perceived to be the highest impact first. Order here has nothing to do with difficulty of design/implementation, only what I perceive to be the impact in reducing parens.

  1. Remove clearly unnecessary parens. Things like
    List.map((x) => ((y) => 0, 0), lst)
    could be printed as
    List.map(x => (y => 0, 0), lst)
    Which is much cleaner. It's also a totally non-invasive change, merely done at print time.
  2. Find another way to represent tuples that doesn't burn developers who liked the list syntax [x, y]. One suggestion, is to find a different separator for [ ] would distinguish tuples from lists. For example, [x . y] could represent tuples, while [x] still represents the list with one item, and [x, y] still represents a list with two items - this would represent no conflicts because there's no such thing as a tuple with exactly one item. If we can do some user studies that show this would work for people who don't yet use Reason as well as people that do, then this could be a nice way to clean up syntax. Please feel free to suggest other alternatives.
  3. Consider <> for type parameters.

I propose that we go down this list, and at each stage reevaluate how important this is. We might find 80% of peoples' issues are solved just by the first improvement. Maybe we decide to go all the way to the end. The cool thing is that I believe all of the proposals in that list are non-breaking changes.

I've been considering semicolon as the separator, and think it'd be an improvement even if parentheses are kept, since it'd significantly help with (visually) distinguishing tuples from argument lists (or list literals if using []) while still being a natural (and therefore in itself more readable) item separator.

```rust
/* (a, b) */
type t = (int, option((int, int))) => int;
let f = ((a, b), x) => ...
let xs = [(a, b), (c, d)];

/* (a; b) */
type t = (int, option((int; int))) => int;
let f = ((a; b), x) => ...
let xs = [(a; b), (c; d)];

/* [a; b] */
type t = (int, option([int; int])) => int;
let f = ([a; b], x) => ...
let xs = [[a; b], [c; d]];

I wish it weren't the case, but I've never seen so much rage induced from a single ascii character than what I see in response to the ; character. It's also especially challenging for us because some have concluded that "Reason has more semicolons used for more purposes than OCaml" - which isn't the case either, and I'd hate to give more ammunition to that argument. Did you not like the . separator?

Hmm, I've not really noticed that rage, just a few people complaining about the semicolon as line/expresssion/statement terminator. I know some are volently opposed to semicolons in that role outside of Reason, but I've not seen anyone complain about the character itself in other roles. I do see the problem with overloading of course, but that's also the problem we're trying to solve with regards to comma and parentheses, which IMO are much worse since those overloads intersect a lot more.

I don't particularly like the idea of . as separator, perhaps mostly because it reads more as a terminator than a separator. Or to a lesser degree as a "path" separator if used without whitespace. In terms of readabiility it would be a bit like replacing all the commas in a sentence with periods.

Compared to semicolon the overloading also seems more easily confused with other meanings:

/* record access */
let (x.y) = (a.b.c . d.e.f);

/* local open */
let x = Json.(Decode.int(a) . Decode.string(b));

/* universally quantified type variables */
let f : a b . (a . b) => a;

I really like the direction @jordwalke proposed. As for the new tuple syntax I dislike the . because it's very similar to , and I think this will hurt the readability (along with other arguments @glennsl listed). As for ; it just feels wrong for some reason, and I see how a lot of other people could feel like that too. Also I think most programmers are used to semicolon being terminator and not a separator, so that argument applies to ; too. To sum up I personally prefer ; over . as I think I could get used to the semicolon, but I agree that it might hurt adoptability.

/* [a ^ b] */
type t = (int, option([int ^ int])) => int;
let f = ([a ^ b], x) => ...
let xs = [[a ^ b], [c ^ d]];

The only conflict I am aware of is the current ref syntax. However I and a bunch of other people dislike the ^ for ref syntax so it might be worth changing that one to something else to allow room for this tuple syntac. I feel that it could work out quite nicely. (I'm personaly using myRef := 1 and myRef.contents for refs in my code currently, even though I used both versions of sugar in Reason 2). Overall my favorite option so far

/* <a, b> */
type t = (int, option(<int, int>)) => int;
let f = (<a, b>, x) => ...
let xs = [<a, b>, <c, d>];

This would conflict with the proposed type parameter syntax, but I still find it much nicer than (a; b) and (a . b)

I don't know why I like it so much better than <> for type parameters, but I quite like <> for tuples.

Just my 2cents but I believe that using <> for generics would be an improvement for Reason. This is the most popular syntax for it: Flow, TypeScript, Hack, C++, Java, C#, Dart, Kotlin, Swift, Rust... are all using <> for generics.

I believe that ocaml/reason generics behave much the same way as all those others from a type system point of view so it would make sense to use the same syntax.

Two more cases that print too many parens, thereby increasing paren fatigue.

Another thing that causes too many parens with a JS like syntax, is the fact that unit is also (). Normally that doesn't cause paren-fatigue, but one case I noticed it often does is in the final "unit" arg to a series of named arguments:

 style=(ReactDOMRe.Style.make(~color="#444444", ~fontSize="68px", ()))

Instead of changing the syntax for unit, I think we can actually change the syntax to automatically provide the final unit arg to any named argument series if there is no final non-named arg, and then allow a syntax to opt out of it.
This sounds heavy handed, but I actually think it could be a good idea in general because:

  • The missing final unit arg is a big surprise to new developers.
  • We still provide a way to express the old behavior - nothing is lost.
  • I believe that all labeled arg functions even without optionals should be providing some final non-named argument: Justification: so that you can later add optional labeled arguments, without having to add a final unit argument, and then update all the call sites. If you always include a final non-named argument for functions that have named args, you can add optional args without having to update any callers.
  • It helps reduce paren fatigue.

Before/after: (much nicer, eh?)

style=(ReactDOMRe.Style.make(~color="#444444", ~fontSize="68px", ()))
style=(ReactDOMRe.Style.make(~color="#444444", ~fontSize="68px"))

Omitting the final unit argument might become a major cause of confusion when used with functions (intuitively) returning 0-ary callbacks, e.g., does this just return a callback or does it also call it? That's a rather important distinction.

reduce(() => Increment(id))

Printing of immediately applied 0-ary callbacks also ought to be improved to better reflect intention. Not sure if doing the same with unit termination arguments is an improvement, but these use cases need to be considered together.

reduce(() => Increment(id), ())
style=(ReactDOMRe.Style.make(~color="#444444", ~fontSize="68px", ()))

/* vs */

reduce(() => Increment(id))()
style=(ReactDOMRe.Style.make(~color="#444444", ~fontSize="68px")())

Omitting the final unit argument might become a major cause of confusion when used with functions (intuitively) returning 0-ary callbacks, e.g., does this just return a callback or does it also call it? That's a rather important distinction.

reduce(() => Increment(id))

My proposal was to only provide a different syntax (that hides - not omits) the final unit arg in the case where there are named arguments already in place before it, and no other arguments after it.

Printing of immediately applied 0-ary callbacks also ought to be improved to better reflect intention. Not sure if doing the same with unit termination arguments is an improvement, but these use cases need to be considered together.

My justification for the named args syntax change was not truly motivated by reducing parens, but by making named args more intuitive with fewer foot-guns. It did have the benefit of reducing parens which is why I mentioned it in this issue.

The immediately invoked unit function might be another one worth special casing in my opinion. I don't think that the same thing we do for named args would apply in the case of immediately invoked unit functions, because the motivations were different. My motivations for named args improvements were to make the syntax more intuitive by default, and the motivation for immediately invoked unit functions is to preserve intention:

I think we could detect and print immediately invoked unit function as:

reduce(() => Increment(id))()

(Have you seen a lot of these immediately invoked unit args in practice? I admit I haven't).

My proposal was to only provide a different syntax (that hides - not omits) the final unit arg in the case where there are named arguments already in place before it, and no other arguments after it.

That could still easily (though perhaps not very likely) intersect with immediately invoked unit funcitons, for example:

reduce(~f=() => Increment(id))

Have you seen a lot of these immediately invoked unit args in practice? I admit I haven't.

It's a pretty frequent pattern with ReasonReact reducers, which the example was meant to allude to.

I understand and agree with your motivation for hiding the unit termination argument. I'm just worried that it might not be possible to properly distinguish it from other meanings of a final unit argument and therefore lead to confusion.

We could make a separate issue to discuss the proposal but you do bring up a good point. I believe the concern is addressed by something I didn’t mention yet : that the transformation also occurs at the definition of any function with named args. That symmetry seems to help solve the problem in your specific example.

Just saw this issue and would like to add my 2 cents. I'm a beginner trying to make sense of the reason-react binding. I think one of the biggest issue with reading the code is the parens. Half of the time I don't know whether I'm reading a parameterized type or function. I am slowly getting used to it. But I think for beginners, making a distinction between types and values would make code clearer. In addition, as @bobzhang alluded to (and from my reading), Ocaml doesn't have HKT and more fundamentally, Ocaml has very different semantics for these 2 worlds. So perhaps reason's syntax should "embrace" this fact and make them more separate rather than trying to unify something that are just different.

here is another example in https://github.com/BuckleScript/bucklescript/issues/2430, quite hard to read:

[@bs.module "fs"] external read_file : (string, (Js.null(Js.Exn.t), Js.null(string)) => unit) => unit = "readFile";

I'm think part of the problem here is that some devs might not have rainbow parens or paredit or similar with their editors, so it's harder for them to read or see. For example, rainbow parens makes the above code much more readable.

For more complex types I don't think there's anything wrong with providing type aliases too if appropriate:

type null_string = Js.null(string);
type null_js_exn = Js.null(Js.Exn.t);
type read_file_callback = (null_js_exn, null_string) => unit;
[@bs.module "fs"] external read_file : (string, read_file_callback) => unit = "readFile";

or with just the callback, which is usually what I always do for types with callbacks:

type read_file_callback = (Js.null(Js.Exn.t), Js.null(string)) => unit;
[@bs.module "fs"] external read_file : (string, read_file_callback) => unit = "readFile";

I don't mind either approach.
<> can imply Java or C# style parameterized types to some people, and for those that have seen that syntax from Java and got burned by it badly, they might be much more reluctant to write types like these because they think it works like Java's when it veery much doesn't.
OTOH, if someone seeing that syntax reminds them of Typescript, they might find it more approachable.

One downside to parens is that it implies that it's a function, which is true in that it does operate like a type level function but with a caveat: You can't partially apply kinds in ocaml (they aren't curried), but you can partially apply functions.

Here's a rewriting of Bob's example:

[@bs.module "fs"]
external read_file : (string, (Js.null(Js.Exn.t), Js.null(string)) => unit) => unit = "readFile";
[@bs.module "fs"]
external read_file : (string, (Js.null<Js.Exn.t>, Js.null<string>) => unit) => unit = "readFile";

I'd say this particular example was a slight win for anyone familiar with reason, and a small but clear readability win for anyone coming from other languages.

The one downside is that <x, y> is one of the only possible viable alternative syntaxes for tuples and we would be giving that up.

I'm on the fence but based on all this feedback I'm slightly leaning towards < > for type parameters. No one would hate < > for type parameters. A good portion (I'd estimate fifty percent) would count < > for type parameters as a small but noticeable win.

I still really think that losing the parens is a mistake. Parens are for function calls, and a type ctor is a function call. It's similar in the theory, so it should be similar in the syntax:

let f(y) = z; /* value */
type f('y) = z; /* type */
module F(Y) = Z; /* module */

@ubsan definitely a good point

I understand and appreciate the symmetry (even if type constructors are not exactly function calls - they are conceptually similar). However, let's not forget that this group on this discussion thread has self-selected into liking the current Reason syntax. We're biased towards the status quo - people who didn't like Reason's syntax as it is are less likely to even be participating and we should also try to put ourselves in their minds.

@jordwalke I've personally used a lot of languages, and one of my favorite features in languages is when type application is syntactically similar to value application - I personally find Idris/Haskell's syntax quite pretty, for example. C++-style <>, mostly, imo, obscures what's really going on.

One thought: If type applications are n-ary, and tuples are n-ary, then it makes sense that they would match, and so you could have <> for both type application and tuples.

@jordwalke That seems like it would open up a lot of visual ambiguity in expressions. For example, <a<b,c>d>.

@jordwalke I feel like then, you have a similar issue - option<<a, b>> feels weird. I would prefer either option<(a, b)> or option(<a, b>) (and I'd definitely prefer the latter)

Agree that moving both to <> will lead to confusing expressions. Also agree with @ubsan that <> seems like a really good solution for tuples which I think is the bigger problem currently (and as @jordwalke mentioned earlier that it should be solved first). The only argument for using <> on types is familiarity for new people, but I'm not sure if it's worth optimizing language for "Hello world" type programs if it means sacrificing other things. I think writing "Hello world"s is not what most of us are using or intend to use Reason for and honestly I think there are probably better languages for that ;)

I think tuples should stay as (). It's standard syntax in every language that has them, it would be like changing arrays from [] to <>

Except that arrays are [||] :) and no one is going crazy about it. On the other hand it's a place where I personally mess up at times so it's a fair point. At the end of the day it's a trade-off and the point was trying to make that we should not be sacrificing stuff just for the sake of familiarity. I do agree that having <> for type parameters is a good idea, but I think it's only a good idea if it goes together with a good solution for tuples. However I don't like it if there is no good solution for tuples and the result is unreadable code (where it uses tuples) just for the sake of better newcomer experience. And what concerns me even more is that having parens in type parameters might not even be an issue anymore once there is a better tuple syntax in place.

@SllyQ Yeah, but at least lists are [ ]. I think Risto's point is it would be like changing a "sequency" thing to <>.

I agree that using <> for tuples opens up far too many ambiguities. It doesn't for types though. If changing type parameters to <> optimizes for hello world, but without any other downsides, then why not do it? Getting a fast/reliable installation setup also optimizes for hello world - which is also good as long as it doesn't impede productivity for real apps.

I might have come out a little more negative than I intended to. Of course it is worth it to improve hello world if there are no downsides. My main concern is just that from my experience >90% cases where I had problems with way too much parens (where I don't understand what's going on anymore) is in the code that uses tuples, thus if we can use <> either for tuples or for type params then I highly prefer tuples because I see that as a much bigger issue.

I found another example where <> for type parameters would be very helpful. Classes:

Compare today's syntax:

class virtual tupleStack('x, 'y)(init, init2) = {
  ...
};

If we were to use <> for type parameters:

class virtual tupleStack<'x, 'y>(init, init2) = {
  ...
};
Was this page helpful?
0 / 5 - 0 ratings