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

generic types - syntax comparison #80

Closed
maiermic opened this issue Apr 14, 2016 · 37 comments
Closed

generic types - syntax comparison #80

maiermic opened this issue Apr 14, 2016 · 37 comments
Labels

Comments

@maiermic
Copy link

Overview and comparison of generic type syntax proposals

This issue lists and compares several syntax proposals that have been proposed and initially discussed in issue #55. It should help to come to a decision which syntax proposal should be used.

Note: The following aspects have not been discussed yet and are not part of this overview/comparison:

Table of Contents

Examples

It might be easier for you to reason about syntax if you see some examples. Therefore, I will describe three function types Transform, Filter and Map and an interface type Collection in different syntaxes. I provide different writing styles of each syntax. For example, you might write Filter or Map with use of Transform or using an inline type expression instead.

Functions

In this section I describe the signature of the three generic function types. Further, I describe each type argument and give an example of an function implementation in JavaScript with its type as a comment.

Transform

Name: Transform

Type arguments: Input, Output

Signature: Transform(Input): Output

Description: A function that takes a value of type Input and returns a value of type Output.

Example: The function len takes a String as parameter and returns its length as type Number. Thus it is of type Transform where Input is String and Output is Number.

// Transform(String): Number
function len(str) {
  return str.length;
}

Filter

Name: Filter

Type arguments: Element

Signature: Filter(Element => Boolean, Element[]): Element[]

Description: Filter elements of type Element in an array. Take a function of type Element => Boolean (is equivalent to Transfrom(Element): Boolean) as first parameter. Takes array of type Element[] as second parameter. Returns an array of type Element[].

Example: An generic implementation might look like this:

// Filter(Element => Boolean, Element[]): Element[]
function filter(predicate, elements) {
  return elements.filter(predicate);
}

The generic type argument Element has to be inferred from the passed arguments of the filter call.

// Transform(Number): Boolean
function isEven(x) {
  return x % 2 === 0;
}

// Transform(String): Boolean
function isShort(str) {
  return str.length < 4;
}

// Number[]
var numbers = [1, 2, 3, 4];

// String[]
var strings = ["rtype", "is", "cool"];

// Filter(Number => Boolean, Number[]): Number[]
filter(isEven,  numbers); // [2, 4]: Number[]

// Filter(String => Boolean, String[]): String[]
filter(isShort, strings); // ["is"]: String[]

// type conflicts if incompatible types are use as type argument `Element`
filter(isEven,  strings); // Error: In "Filter" the first type argument "Element" couldn't match both (incompatible) types Number and String
filter(isShort, numbers); // Error: In "Filter" the first type argument "Element" couldn't match both (incompatible) types String and Number

Map

Name: Map

Type arguments: Input, Output

Signature: Map(Input => Output, Input[]): Output[]

Description: Filter elements of type Element in an array. Take a function of type Element => Boolean (is equivalent to Transfrom(Element): Boolean) as first parameter. Takes array of type Element[] as second parameter. Returns an array of type Element[].

Example: Map elements of type Input to type Output. Take a function of type Input => Output (is equivalent to Transfrom(Input): Output) as first parameter. Takes array of type Input[] as second parameter. Returns an array of type Output[].

// Map(Input => Output, Input[]): Output[]
function map(transform, elements) {
  return elements.map(transform);
}

The generic type arguments Input and Output have to be inferred from the passed arguments of the map call.

// Transform(Number): Number
function square(x) {
  return x * x;
}

// Transform(String): Number
function len(str) {
  return str.length;
}

// Number[]
var numbers = [1, 2, 3, 4];

// String[]
var strings = ["rtype", "is", "cool"];

// Map(Number => Number, Number[]): Number[]
map(square, numbers); // [1, 4, 9, 16]: Number[]

// Map(String => Number, String[]): Number[]
map(len,    strings); // [5, 2, 4]: Number[]

map(square, strings); // Error: In "Map" the first type argument "Input" couldn't match both (incompatible) types Number and String
map(len,    numbers); // Error: In "Map" the first type argument "Input" couldn't match both (incompatible) types String and Number

Interface

To keep it simple, I describe an interface Collection that is satisfied by most common data structure objects (for example Array).

Name: Collection

Type arguments: Element

Signature:

// takes a type argument Element
interface Collection {
  filter(Element => Boolean): Collection // with elements of type `Element`

  // map is generic and takes an additional type argument `NewElement`
  map(Element => NewElement): Collection // with elements of type `NewElement`
}

Description: The interface Collection takes one type argument Element that is visible in the interface body. All elements in the data structure are of this type Element. A Collection has methods filter and map. They do the same as the functions filter and map I described in the chapter above, but since they are methods, the data structure is (implicitly) passed as this argument. Hence, the last argument of the corresponding function is dropped in the method.

Example: Let's see some JavaScript code with Array as Collection to see the difference between the function filter and the method filter

// Number => Boolean
function isEven(x) {
  return x % 2 === 0;
}

// Number[]
// Satisfies Collection where type argument Element is Number
var numbers = [1, 2, 3, 4];

// Filter(Number => Boolean, Number[]): Number[]
filter(isEven, numbers); // [2, 4]: Number[]

// numbers: Collection where type argument Element is Number
// numbers.filter: (Number => Boolean): Number[]
numbers.filter(isEven);  // [2, 4]: Number[] which satisfies Collection where type argument Element is Number

and the difference between the function map and the method map:

// Transform(String): Number
function len(str) {
  return str.length;
}

// String[]
// Satisfies Collection where type argument Element is String
var strings = ["rtype", "is", "cool"];

// Map(String => Number, String[]): Number[]
map(len, strings); // [5, 2, 4]: Number[]

// numbers: Collection where type argument Element is String
// numbers.map: (String => Number): Number[]
strings.map(len);  // [5, 2, 4]: Number[] which satisfies Collection where type argument Element is Number

Syntax proposals

Listed in the subchapters below are type declarations of the example types Transform, Filter, Map and Collection written in different variants of each syntax proposal. In all syntax proposals the name of the type is followed by its type arguments followed by its type definition. The difference is how type arguments are written. Are they enclosed in angle or square brackets, parentheses or are they only separated by whitespace. The different variants of a syntax vary in several aspects that might influence readability:

  • with or without colons after type signature
  • multi- vs. single-line
  • (some) type expressions enclosed in parentheses: Input => Output vs. (Input) => Output vs. (Input => Output) used in a specific context
  • additional type argument declaration before or after the method name (here map)

Angle Brackets Syntax

Type arguments are enclosed in angle brackets and are separated by commas. This syntax is influenced by C++, Java, C# and TypeScript.

single-line without colons

Functions
Transform<Input, Output> Input => Output

Transform<Input, Output> (Input) => Output

Transform<Input, Output> (Input => Output)

Filter<Element> (Element => Boolean, Element[]) => Element[]

Filter<Element> (Transform<Element, Boolean>, Element[]) => Element[]

Map<Input, Output> (Input => Output, Input[]) => Output[]

Map<Input, Output> (Transform<Input, Output>, Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection<Element> {
  filter(Element => Boolean) => Collection<Element>

  filter(Transform<Element, Boolean>) => Collection<Element>

  <NewElement> map(transform: Element => NewElement) => Collection<NewElement>

  <NewElement> map(transform: Transform<Element, NewElement>) => Collection<NewElement>

  <NewElement> map(Transform<Element, NewElement>) => Collection<NewElement>
}
Generic type arguments of methods after method name
interface Collection<Element> {
  filter(Element => Boolean) => Collection<Element>

  filter(Transform<Element, Boolean>) => Collection<Element>

  map<NewElement>(transform: Element => NewElement) => Collection<NewElement>

  map<NewElement>(transform: Transform<Element, NewElement>) => Collection<NewElement>

  map<NewElement>(Transform<Element, NewElement>) => Collection<NewElement>
}

single-line with colons

Functions
Transform<Input, Output>: Input => Output

Transform<Input, Output>: (Input) => Output

Transform<Input, Output>: (Input => Output)

Filter<Element>: (Element => Boolean, Element[]) => Element[]

Filter<Element>: (Transform<Element, Boolean>, Element[]) => Element[]

Map<Input, Output>: (Input => Output, Input[]) => Output[]

Map<Input, Output>: (Transform<Input, Output>, Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection<Element> {
  filter: (Element => Boolean) => Collection<Element>

  filter: (Transform<Element, Boolean>) => Collection<Element>

  <NewElement> map: (transform: Element => NewElement) => Collection<NewElement>

  <NewElement> map: (transform: Transform<Element, NewElement>) => Collection<NewElement>

  <NewElement> map: (Transform<Element, NewElement>) => Collection<NewElement>
}
Generic type arguments of methods after method name
interface Collection<Element> {
  filter: (Element => Boolean) => Collection<Element>

  filter: (Transform<Element, Boolean>) => Collection<Element>

  map<NewElement>: (transform: Element => NewElement) => Collection<NewElement>

  map<NewElement>: (transform: Transform<Element, NewElement>) => Collection<NewElement>

  map<NewElement>: (Transform<Element, NewElement>) => Collection<NewElement>
}

multiline with colons

Functions
Transform<Input, Output>:
  Input => Output

Transform<Input, Output>:
  (Input) => Output

Transform<Input, Output>:
  (Input => Output)

Filter<Element>:
  (Element => Boolean, Element[]) => Element[]

Filter<Element>:
  (Transform<Element, Boolean>, Element[]) => Element[]

Map<Input, Output>:
  (Input => Output, Input[]) => Output[]

Map<Input, Output>:
  (Transform<Input, Output>, Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection<Element> {
  filter:
    (Element => Boolean) => Collection<Element>

  filter:
    (Transform<Element, Boolean>) => Collection<Element>

  <NewElement> map:
    (transform: Element => NewElement) => Collection<NewElement>

  <NewElement> map:
    (transform: Transform<Element, NewElement>) => Collection<NewElement>

  <NewElement> map:
    (Transform<Element, NewElement>) => Collection<NewElement>
}
Generic type arguments of methods after method name
interface Collection<Element> {
  filter:
    (Element => Boolean) => Collection<Element>

  filter:
    (Transform<Element, Boolean>) => Collection<Element>

  map<NewElement>:
    (transform: Element => NewElement) => Collection<NewElement>

  map<NewElement>:
    (transform: Transform<Element, NewElement>) => Collection<NewElement>

  map<NewElement>:
    (Transform<Element, NewElement>) => Collection<NewElement>
}

multiline with curly brackets

Functions
Transform<Input, Output> {
  Input => Output
}

Transform<Input, Output> {
  (Input) => Output
}

Transform<Input, Output> {
  (Input => Output)
}

Filter<Element> {
  (Element => Boolean, Element[]) => Element[]
}

Filter<Element> {
  (Transform<Element, Boolean>, Element[]) => Element[]
}

Map<Input, Output> {
  (Input => Output, Input[]) => Output[]
}

Map<Input, Output> {
  (Transform<Input, Output>, Input[]) => Output[]
}
Interface
Generic type arguments of methods before method name
interface Collection<Element> {
  filter {
    (Element => Boolean) => Collection<Element>
  }

  filter {
    (Transform<Element, Boolean>) => Collection<Element>
  }

  <NewElement> map {
    (transform: Element => NewElement) => Collection<NewElement>
  }

  <NewElement> map {
    (transform: Transform<Element, NewElement>) => Collection<NewElement>
  }

  <NewElement> map {
    (Transform<Element, NewElement>) => Collection<NewElement>
  }
}
Generic type arguments of methods after method name
interface Collection<Element> {
  filter {
    (Element => Boolean) => Collection<Element>
  }

  filter {
    (Transform<Element, Boolean>) => Collection<Element>
  }

  map<NewElement> {
    (transform: Element => NewElement) => Collection<NewElement>
  }

  map<NewElement> {
    (transform: Transform<Element, NewElement>) => Collection<NewElement>
  }

  map<NewElement> {
    (Transform<Element, NewElement>) => Collection<NewElement>
  }
}

Square Brackets Syntax

Type arguments are enclosed in square brackets and are separated by commas. This syntax is influenced by Scala.

single-line without colons

Functions
Transform[Input, Output] Input => Output

Transform[Input, Output] (Input) => Output

Transform[Input, Output] (Input => Output)

Filter[Element] (Element => Boolean, Element[]) => Element[]

Filter[Element] (Transform[Element, Boolean], Element[]) => Element[]

Map[Input, Output] (Input => Output, Input[]) => Output[]

Map[Input, Output] (Transform[Input, Output], Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection[Element] {
  filter(Element => Boolean) => Collection[Element]

  filter(Transform[Element, Boolean]) => Collection[Element]

  [NewElement] map(transform: Element => NewElement) => Collection[NewElement]

  [NewElement] map(transform: Transform[Element, NewElement]) => Collection[NewElement]

  [NewElement] map(Transform[Element, NewElement]) => Collection[NewElement]
}
Generic type arguments of methods after method name
interface Collection[Element] {
  filter(Element => Boolean) => Collection[Element]

  filter(Transform[Element, Boolean]) => Collection[Element]

  map[NewElement](transform: Element => NewElement) => Collection[NewElement]

  map[NewElement](transform: Transform[Element, NewElement]) => Collection[NewElement]

  map[NewElement](Transform[Element, NewElement]) => Collection[NewElement]
}

single-line with colons

Functions
Transform[Input, Output]: Input => Output

Transform[Input, Output]: (Input) => Output

Transform[Input, Output]: (Input => Output)

Filter[Element]: (Element => Boolean, Element[]) => Element[]

Filter[Element]: (Transform[Element, Boolean], Element[]) => Element[]

Map[Input, Output]: (Input => Output, Input[]) => Output[]

Map[Input, Output]: (Transform[Input, Output], Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection[Element] {
  filter: (Element => Boolean) => Collection[Element]

  filter: (Transform[Element, Boolean]) => Collection[Element]

  [NewElement] map: (transform: Element => NewElement) => Collection[NewElement]

  [NewElement] map: (transform: Transform[Element, NewElement]) => Collection[NewElement]

  [NewElement] map: (Transform[Element, NewElement]) => Collection[NewElement]
}
Generic type arguments of methods after method name
interface Collection[Element] {
  filter: (Element => Boolean) => Collection[Element]

  filter: (Transform[Element, Boolean]) => Collection[Element]

  map[NewElement]: (transform: Element => NewElement) => Collection[NewElement]

  map[NewElement]: (transform: Transform[Element, NewElement]) => Collection[NewElement]

  map[NewElement]: (Transform[Element, NewElement]) => Collection[NewElement]
}

multiline with colons

Functions
Transform[Input, Output]:
  Input => Output

Transform[Input, Output]:
  (Input) => Output

Transform[Input, Output]:
  (Input => Output)

Filter[Element]:
  (Element => Boolean, Element[]) => Element[]

Filter[Element]:
  (Transform[Element, Boolean], Element[]) => Element[]

Map[Input, Output]:
  (Input => Output, Input[]) => Output[]

Map[Input, Output]:
  (Transform[Input, Output], Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection[Element] {
  filter:
    (Element => Boolean) => Collection[Element]

  filter:
    (Transform[Element, Boolean]) => Collection[Element]

  [NewElement] map:
    (transform: Element => NewElement) => Collection[NewElement]

  [NewElement] map:
    (transform: Transform[Element, NewElement]) => Collection[NewElement]

  [NewElement] map:
    (Transform[Element, NewElement]) => Collection[NewElement]
}
Generic type arguments of methods after method name
interface Collection[Element] {
  filter:
    (Element => Boolean) => Collection[Element]

  filter:
    (Transform[Element, Boolean]) => Collection[Element]

  map[NewElement]:
    (transform: Element => NewElement) => Collection[NewElement]

  map[NewElement]:
    (transform: Transform[Element, NewElement]) => Collection[NewElement]

  map[NewElement]:
    (Transform[Element, NewElement]) => Collection[NewElement]
}

multiline with curly brackets

Functions
Transform[Input, Output] {
  Input => Output
}

Transform[Input, Output] {
  (Input) => Output
}

Transform[Input, Output] {
  (Input => Output)
}

Filter[Element] {
  (Element => Boolean, Element[]) => Element[]
}

Filter[Element] {
  (Transform[Element, Boolean], Element[]) => Element[]
}

Map[Input, Output] {
  (Input => Output, Input[]) => Output[]
}

Map[Input, Output] {
  (Transform[Input, Output], Input[]) => Output[]
}
Interface
Generic type arguments of methods before method name
interface Collection[Element] {
  filter {
    (Element => Boolean) => Collection[Element]
  }

  filter {
    (Transform[Element, Boolean]) => Collection[Element]
  }

  [NewElement] map {
    (transform: Element => NewElement) => Collection[NewElement]
  }

  [NewElement] map {
    (transform: Transform[Element, NewElement]) => Collection[NewElement]
  }

  [NewElement] map {
    (Transform[Element, NewElement]) => Collection[NewElement]
  }
}
Generic type arguments of methods after method name
interface Collection[Element] {
  filter {
    (Element => Boolean) => Collection[Element]
  }

  filter {
    (Transform[Element, Boolean]) => Collection[Element]
  }

  map[NewElement] {
    (transform: Element => NewElement) => Collection[NewElement]
  }

  map[NewElement] {
    (transform: Transform[Element, NewElement]) => Collection[NewElement]
  }

  map[NewElement] {
    (Transform[Element, NewElement]) => Collection[NewElement]
  }
}

Parentheses Syntax

Type arguments are enclosed in parentheses and are separated by commas. This syntax is influenced by JavaScript function notation. Since a generic type is a type constructor/function that takes types as arguments and returns a concrete type.

single-line without colons

Functions
Transform(Input, Output) Input => Output

Transform(Input, Output) (Input) => Output

Transform(Input, Output) (Input => Output)

Filter(Element) (Element => Boolean, Element[]) => Element[]

Filter(Element) (Transform(Element, Boolean), Element[]) => Element[]

Map(Input, Output) (Input => Output, Input[]) => Output[]

Map(Input, Output) (Transform(Input, Output), Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection(Element) {
  filter(Element => Boolean) => Collection(Element)

  filter(Transform(Element, Boolean)) => Collection(Element)

  (NewElement) map(transform: Element => NewElement) => Collection(NewElement)

  (NewElement) map(transform: Transform(Element, NewElement)) => Collection(NewElement)

  (NewElement) map(Transform(Element, NewElement)) => Collection(NewElement)
}
Generic type arguments of methods after method name
interface Collection(Element) {
  filter(Element => Boolean) => Collection(Element)

  filter(Transform(Element, Boolean)) => Collection(Element)

  map(NewElement)(transform: Element => NewElement) => Collection(NewElement)

  map(NewElement)(transform: Transform(Element, NewElement)) => Collection(NewElement)

  map(NewElement)(Transform(Element, NewElement)) => Collection(NewElement)
}

single-line with colons

Functions
Transform(Input, Output): Input => Output

Transform(Input, Output): (Input) => Output

Transform(Input, Output): (Input => Output)

Filter(Element): (Element => Boolean, Element[]) => Element[]

Filter(Element): (Transform(Element, Boolean), Element[]) => Element[]

Map(Input, Output): (Input => Output, Input[]) => Output[]

Map(Input, Output): (Transform(Input, Output), Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection(Element) {
  filter: (Element => Boolean) => Collection(Element)

  filter: (Transform(Element, Boolean)) => Collection(Element)

  (NewElement) map: (transform: Element => NewElement) => Collection(NewElement)

  (NewElement) map: (transform: Transform(Element, NewElement)) => Collection(NewElement)

  (NewElement) map: (Transform(Element, NewElement)) => Collection(NewElement)
}
Generic type arguments of methods after method name
interface Collection(Element) {
  filter: (Element => Boolean) => Collection(Element)

  filter: (Transform(Element, Boolean)) => Collection(Element)

  map(NewElement): (transform: Element => NewElement) => Collection(NewElement)

  map(NewElement): (transform: Transform(Element, NewElement)) => Collection(NewElement)

  map(NewElement): (Transform(Element, NewElement)) => Collection(NewElement)
}

multiline with colons

Functions
Transform(Input, Output):
  Input => Output

Transform(Input, Output):
  (Input) => Output

Transform(Input, Output):
  (Input => Output)

Filter(Element):
  (Element => Boolean, Element[]) => Element[]

Filter(Element):
  (Transform(Element, Boolean), Element[]) => Element[]

Map(Input, Output):
  (Input => Output, Input[]) => Output[]

Map(Input, Output):
  (Transform(Input, Output), Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection(Element) {
  filter:
    (Element => Boolean) => Collection(Element)

  filter:
    (Transform(Element, Boolean)) => Collection(Element)

  (NewElement) map:
    (transform: Element => NewElement) => Collection(NewElement)

  (NewElement) map:
    (transform: Transform(Element, NewElement)) => Collection(NewElement)

  (NewElement) map:
    (Transform(Element, NewElement)) => Collection(NewElement)
}
Generic type arguments of methods after method name
interface Collection(Element) {
  filter:
    (Element => Boolean) => Collection(Element)

  filter:
    (Transform(Element, Boolean)) => Collection(Element)

  map(NewElement):
    (transform: Element => NewElement) => Collection(NewElement)

  map(NewElement):
    (transform: Transform(Element, NewElement)) => Collection(NewElement)

  map(NewElement):
    (Transform(Element, NewElement)) => Collection(NewElement)
}

multiline with curly brackets

Functions
Transform(Input, Output) {
  Input => Output
}

Transform(Input, Output) {
  (Input) => Output
}

Transform(Input, Output) {
  (Input => Output)
}

Filter(Element) {
  (Element => Boolean, Element[]) => Element[]
}

Filter(Element) {
  (Transform(Element, Boolean), Element[]) => Element[]
}

Map(Input, Output) {
  (Input => Output, Input[]) => Output[]
}

Map(Input, Output) {
  (Transform(Input, Output), Input[]) => Output[]
}
Interface
Generic type arguments of methods before method name
interface Collection(Element) {
  filter {
    (Element => Boolean) => Collection(Element)
  }

  filter {
    (Transform(Element, Boolean)) => Collection(Element)
  }

  (NewElement) map {
    (transform: Element => NewElement) => Collection(NewElement)
  }

  (NewElement) map {
    (transform: Transform(Element, NewElement)) => Collection(NewElement)
  }

  (NewElement) map {
    (Transform(Element, NewElement)) => Collection(NewElement)
  }
}
Generic type arguments of methods after method name
interface Collection(Element) {
  filter {
    (Element => Boolean) => Collection(Element)
  }

  filter {
    (Transform(Element, Boolean)) => Collection(Element)
  }

  map(NewElement) {
    (transform: Element => NewElement) => Collection(NewElement)
  }

  map(NewElement) {
    (transform: Transform(Element, NewElement)) => Collection(NewElement)
  }

  map(NewElement) {
    (Transform(Element, NewElement)) => Collection(NewElement)
  }
}

Whitespace Syntax

Type arguments are written without backets and are separated by whitespace. This syntax is influenced by Elm and Haskell.

single-line without colons

Functions
Transform Input Output Input => Output

Transform Input Output (Input) => Output

Transform Input Output (Input => Output)

Filter Element (Element => Boolean, Element[]) => Element[]

Filter Element (Transform Element Boolean, Element[]) => Element[]

Map Input Output (Input => Output, Input[]) => Output[]

Map Input Output (Transform Input Output, Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection Element {
  filter(Element => Boolean) => Collection Element

  filter(Transform Element Boolean) => Collection Element

   NewElement map(transform: Element => NewElement) => Collection NewElement

   NewElement map(transform: Transform Element NewElement) => Collection NewElement

   NewElement map(Transform Element NewElement) => Collection NewElement
}
Generic type arguments of methods after method name
interface Collection Element {
  filter(Element => Boolean) => Collection Element

  filter(Transform Element Boolean) => Collection Element

  map NewElement(transform: Element => NewElement) => Collection NewElement

  map NewElement(transform: Transform Element NewElement) => Collection NewElement

  map NewElement(Transform Element NewElement) => Collection NewElement
}

single-line with colons

Functions
Transform Input Output: Input => Output

Transform Input Output: (Input) => Output

Transform Input Output: (Input => Output)

Filter Element: (Element => Boolean, Element[]) => Element[]

Filter Element: (Transform Element Boolean, Element[]) => Element[]

Map Input Output: (Input => Output, Input[]) => Output[]

Map Input Output: (Transform Input Output, Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection Element {
  filter: (Element => Boolean) => Collection Element

  filter: (Transform Element Boolean) => Collection Element

   NewElement map: (transform: Element => NewElement) => Collection NewElement

   NewElement map: (transform: Transform Element NewElement) => Collection NewElement

   NewElement map: (Transform Element NewElement) => Collection NewElement
}
Generic type arguments of methods after method name
interface Collection Element {
  filter: (Element => Boolean) => Collection Element

  filter: (Transform Element Boolean) => Collection Element

  map NewElement: (transform: Element => NewElement) => Collection NewElement

  map NewElement: (transform: Transform Element NewElement) => Collection NewElement

  map NewElement: (Transform Element NewElement) => Collection NewElement
}

multiline with colons

Functions
Transform Input Output:
  Input => Output

Transform Input Output:
  (Input) => Output

Transform Input Output:
  (Input => Output)

Filter Element:
  (Element => Boolean, Element[]) => Element[]

Filter Element:
  (Transform Element Boolean, Element[]) => Element[]

Map Input Output:
  (Input => Output, Input[]) => Output[]

Map Input Output:
  (Transform Input Output, Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection Element {
  filter:
    (Element => Boolean) => Collection Element

  filter:
    (Transform Element Boolean) => Collection Element

   NewElement map:
    (transform: Element => NewElement) => Collection NewElement

   NewElement map:
    (transform: Transform Element NewElement) => Collection NewElement

   NewElement map:
    (Transform Element NewElement) => Collection NewElement
}
Generic type arguments of methods after method name
interface Collection Element {
  filter:
    (Element => Boolean) => Collection Element

  filter:
    (Transform Element Boolean) => Collection Element

  map NewElement:
    (transform: Element => NewElement) => Collection NewElement

  map NewElement:
    (transform: Transform Element NewElement) => Collection NewElement

  map NewElement:
    (Transform Element NewElement) => Collection NewElement
}

multiline with curly brackets

Functions
Transform Input Output {
  Input => Output
}

Transform Input Output {
  (Input) => Output
}

Transform Input Output {
  (Input => Output)
}

Filter Element {
  (Element => Boolean, Element[]) => Element[]
}

Filter Element {
  (Transform Element Boolean, Element[]) => Element[]
}

Map Input Output {
  (Input => Output, Input[]) => Output[]
}

Map Input Output {
  (Transform Input Output, Input[]) => Output[]
}
Interface
Generic type arguments of methods before method name
interface Collection Element {
  filter {
    (Element => Boolean) => Collection Element
  }

  filter {
    (Transform Element Boolean) => Collection Element
  }

   NewElement map {
    (transform: Element => NewElement) => Collection NewElement
  }

   NewElement map {
    (transform: Transform Element NewElement) => Collection NewElement
  }

   NewElement map {
    (Transform Element NewElement) => Collection NewElement
  }
}
Generic type arguments of methods after method name
interface Collection Element {
  filter {
    (Element => Boolean) => Collection Element
  }

  filter {
    (Transform Element Boolean) => Collection Element
  }

  map NewElement {
    (transform: Element => NewElement) => Collection NewElement
  }

  map NewElement {
    (transform: Transform Element NewElement) => Collection NewElement
  }

  map NewElement {
    (Transform Element NewElement) => Collection NewElement
  }
}

Comparison of syntax proposals

It is hard to compare syntax impersonal and objectively. Opinions might differ on readability and the question if familiarity of a syntax is an advantage or an disadvantage. Thus, I mainly collect opinions as quotations. I will add opinions that arise in further discussion. Please tell me if any opinion or aspect is missing or if something is not quite appropriate.

Angle Brackets Syntax

Similar to C++ templates and Java, C# and TypeScript generics.

Thousands of C++/Java/C# developers are currently being converted to JS developers, you all know that. Students in (high)schools study the C++/Java/C#. I would recommend to make sure these people can read rtype easily without browsing the documentation (JSDoc is quite readable, btw, thus popular).
[...]
TypeScript syntax is readable to most developers
-- koresar

The problem is that the <> syntax isn't consistent between languages anyway.
Look at Java, C#, Kotlin, Rust, etc. all of them are doing it differently.
-- unknown reddit user

TypeScript's generics in function signatures, and the TypeScript syntax feels like an assault on my senses
-- ericelliott

I think there's something about the angle brackets that drives me crazy. Reminds me of C++ templates or something (which I have bad memories of).
-- ericelliott

👎 For angle brackets
-- BerkeleyTrue

Java's approach is saying "f*ck it", let's just have stuff like method<String>(), but instance.<String>method() at use-site; and at declaration-site (different issue) it's even more different with class Box<T> vs. <T> void foo(T t).
-- unknown reddit user

Advantages

  • common syntax in many languages

Disadvatages

  • < (Unicode Character 'LESS-THAN SIGN') and > (Unicode Character 'GREATER-THAN SIGN') used as brackets are harder to read, since they are no real brackets. The real angle brackets are 〈 (Unicode Character 'LEFT ANGLE BRACKET') and 〉 (Unicode Character 'RIGHT ANGLE BRACKET').

  • parsing issues

    Using <> for generics is a historical accident. If you look at all the languages which adopted it, all of them need horrible hacks to work around the intrinsic issues.
    -- unknown reddit user

    Do you have any examples where they had to work around these parsing issues due to using <> for generics?

    C# and Kotlin for example just keep reading after the < and have a huge list of tokens to determine whether it's a comparison, Generics, or if they need to keep reading further. They basically need unlimited lookahead, and if they figure it out, they rollback the parser state and parse it again, with more information.
    -- unknown reddit user

    In return you avoid the compiler having to look ahead a little during parsing. Is that really a good trade? I don't.

    If even the parser has trouble reading, people will very likely also have trouble with comprehending things.
    -- unknown reddit user

    In case of a single-parameter-function as concrete type argument readability is decreased with angle brackets notation due to the fact that a closing angle bracket is used as arrow head:

     MyInterface<String => String>   // angle bracket syntax
    
     MyInterface<(String => String)> // angle bracket syntax, IMO parentheses don't increase readability in this case
    
     MyInterface (String => String)  // whitespace syntax (type argument has to be enclosed in parentheses, otherwise it would be equal to `(MyInterface String) => String`
    
     MyInterface(String => String)   // parentheses/function syntax; Note: Could also be written like whitespace syntax in this case: `MyInterface (String => String)`

    -- maiermic

    map<t, u>(fn: (x: t) => u, list: t[]) => u[];
    IMO, this is even worse than the C++ & Java versions because of the arrow functions. Note how the closing angle bracket for the type parameter declaration looks visually very similar to the arrows in the arrow functions. My brain tries to match them up.

    My brain also struggles with the groupings of type parameters and function parameters, mostly lost in the syntax noise.

    [...]

    interface StringStringThing:
      MyInterface<String => String>

    Which visually looks a bit like this:

    interface StringStringThing:
      MyInterface<String => // wtf is the rest of that nonsense?

    -- ericelliott

Square Brackets Syntax

Similar to Scala generics

Java's approach is saying "f*ck it", let's just have stuff like method<String>(), but instance.<String>method() at use-site; and at declaration-site (different issue) it's even more different with class Box<T> vs. <T> void foo(T t).
In Scala, everything is regular and consistent. The [] are always at the same place, everywhere.
-- unknown reddit user

Advantages

  • consistency: every time you see [] (in rtype) a generic type is described

Disadvatages

  • every time you see [] in JavaScript an object property or array element is accessed.
  • conflicts with current rtype array syntax
    • might be resolved by writting [Number] instead of Number[]

      We could write Array[T] instead of T[] in rtype. We might use [T] as short version of Array[T]. One benefit of writing the generic type in brackets is better readablity of type expressions:
      (Number | String)[] vs. [Number | String] or Array[Number | String]
      -- maiermic

Parentheses Syntax

Similar to a regular JavaScript function.

Advantages:

  • it looks similar to a function declaration
  • it behaves similar to a function declaration: defines a type constructor (= function) that takes types as parameters and returns a type

-- maiermic

Disadvantages:

  • it looks similar to a function declaration
  • it behaves similar to a function declaration: defines a type constructor (= function) that takes types as parameters and returns a type

Sorry for trolling like messages. That's my personal opinion. We should lean towards readability. Using parenthesis for everything damages readability significantly. (One of the reasons to not like Lisp is the parenthesis bloat.)
-- koresar

Advantages

  • type constructor are functions

    Generic types like Map are not just type names. They are type constructors:

    type constructor is a feature of a typed formal language that builds new types from old ones
    Wikipedia - Type constructor

    That means a type constructor is not a type itself, but it describes a class of types.
    -- maiermic

    A type constructor is more like a function that returns a type:

    // type constructor Map
    function Map(t: Type, u: Type): Type {
      return ((x: t) => u, list: t[]) => u[]
    }
    

    -- maiermic

Disadvatages

  • a type constructor might get confused with a regular function
  • if types are inferred, type arguments are passed implicitly, which is not possible in regular JavaScript functions

Whitespace Syntax

Similar to Elm and Haskell.

Advantages

  • fewer characters (no special brackets) are needed
  • reusable/composable: due to curring, partial function application of type constructors is possible

Disadvatages

  • whitespace syntax introduces currying. Most JavaScript developers are not familiar with this concept of function notation.

    Haskell is known but an exotic language yet. For C++/Java/C# developers it is counter intuitive that the space symbol have a special syntactic meaning. Frankly speaking, I still do not understand what's that: Transform t u: (x: t) => u.
    -- koresar

@Raynos
Copy link
Collaborator

Raynos commented Apr 17, 2016

I've implemented generics in my type checker. I've added experimental support for rtype to the type checker in #89

The syntax in terms of usage is

interface TDB {
    _values: Object<String, String>,
}

The syntax in terms of definition is (not fully supported in the checker yet...) but would look like

interface TDB<T> {
    _values: Object<String, T>,

    get<T>(key: String) => T,
    set<T>(key: String, value: T) => void,
    keys() => Array<String>
}

I've gone for re-using the T in the function signature because it's really only functions that close over a T. Note that get<T>(key: String) => T is a shorthand for get: <T>(this: TDB<T>, key: String) => T.

Generic resolution is done on a per callsite basis, we infer what T should be and then type check both the callsite and implementation of the function get.

@maiermic maiermic mentioned this issue Apr 17, 2016
@ericelliott
Copy link
Owner

@Raynos Very cool! Just one problem: We haven't decided on the syntax for generics in rtype. );

Personally, I prefer both the square bracket and spaced notations over angle brackets. Square brackets everywhere. We could make [T] short for Array[T]. I think that arrow functions in signatures are a fairly strong argument against angle bracket notation for JS.

What do you think?

@Raynos
Copy link
Collaborator

Raynos commented Apr 18, 2016

@ericelliott I do not have strong opinions on syntax.

interface TDB[T] {
    _values: Object[String, T],

    get[T](key: String) => T,
    set[T](key: String, value: T) => void,
    keys() => Array<String>
}

Is fine with me.

@ericelliott
Copy link
Owner

Let's try that and see how it goes. Can you implement it in your type checker and report back if it causes any problems?

@Raynos
Copy link
Collaborator

Raynos commented Apr 18, 2016

Generics is still a feature branch ( https://github.com/Raynos/jsig2/tree/implement-generics ) that needs more work, once its in master I'll try the square brackets syntax.

@ericelliott
Copy link
Owner

Very cool. Keep me posted. =)

@tomek-he-him
Copy link
Collaborator

Whoah, there’s a lot happening here over the last weeks! Sorry for being an outsider, it’s a very intensive time in my life right now. With my second son born last week, emailing and travelling all over in search of a remote job (not that easy in Europe), preparing to move house, preparing the final release of my current big work project, handing projects over before quitting the job, etc, etc, etc, etc. So yeah, sorry – I hope you understand.

I’m hoping to jump back in next month.

Anyway, just one quick thought that popped into my mind while reading this fantastic issue. @maiermic mentioned that the <> syntax is just a poor man’s variation on real angle brackets (〈〉) or mathematical angle brackets (⟨⟩). Now that probably every developer’s text editor renders Unicode characters properly, only inputting the characters might be a problem. However, there are already whole programming languages (e. g. Agda) where non-ASCII characters have semantic meaning. If you want to add this option for discussion, let me know and I’ll post an example similar to those by @maiermic.

@tomek-he-him
Copy link
Collaborator

I actually have it already, so I’ll post the proposal. If non-ASCII characters are not an option though, feel free to delete the comment so it doesn’t clutter the discussion.


Real Angle Brackets Syntax

Type arguments are enclosed in real angle brackets and are separated by commas. This syntax is influenced by C++, Java, C# and TypeScript, but the ASCII brackets they use (<>) are replaced by real angle brackets signs (〈〉).

single-line without colons

Functions
Transform〈Input, Output〉 Input => Output

Transform〈Input, Output〉 (Input) => Output

Transform〈Input, Output〉 (Input => Output)

Filter〈Element〉 (Element => Boolean, Element[]) => Element[]

Filter〈Element〉 (Transform〈Element, Boolean〉, Element[]) => Element[]

Map〈Input, Output〉 (Input => Output, Input[]) => Output[]

Map〈Input, Output〉 (Transform〈Input, Output〉, Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection〈Element〉 {
  filter(Element => Boolean) => Collection〈Element〉

  filter(Transform〈Element, Boolean〉) => Collection〈Element〉

  〈NewElement〉 map(transform: Element => NewElement) => Collection〈NewElement〉

  〈NewElement〉 map(transform: Transform〈Element, NewElement〉) => Collection〈NewElement〉

  〈NewElement〉 map(Transform〈Element, NewElement〉) => Collection〈NewElement〉
}
Generic type arguments of methods after method name
interface Collection〈Element〉 {
  filter(Element => Boolean) => Collection〈Element〉

  filter(Transform〈Element, Boolean〉) => Collection〈Element〉

  map〈NewElement〉(transform: Element => NewElement) => Collection〈NewElement〉

  map〈NewElement〉(transform: Transform〈Element, NewElement〉) => Collection〈NewElement〉

  map〈NewElement〉(Transform〈Element, NewElement〉) => Collection〈NewElement〉
}

single-line with colons

Functions
Transform〈Input, Output〉: Input => Output

Transform〈Input, Output〉: (Input) => Output

Transform〈Input, Output〉: (Input => Output)

Filter〈Element〉: (Element => Boolean, Element[]) => Element[]

Filter〈Element〉: (Transform〈Element, Boolean〉, Element[]) => Element[]

Map〈Input, Output〉: (Input => Output, Input[]) => Output[]

Map〈Input, Output〉: (Transform〈Input, Output〉, Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection〈Element〉 {
  filter: (Element => Boolean) => Collection〈Element〉

  filter: (Transform〈Element, Boolean〉) => Collection〈Element〉

  〈NewElement〉 map: (transform: Element => NewElement) => Collection〈NewElement〉

  〈NewElement〉 map: (transform: Transform〈Element, NewElement〉) => Collection〈NewElement〉

  〈NewElement〉 map: (Transform〈Element, NewElement〉) => Collection〈NewElement〉
}
Generic type arguments of methods after method name
interface Collection〈Element〉 {
  filter: (Element => Boolean) => Collection〈Element〉

  filter: (Transform〈Element, Boolean〉) => Collection〈Element〉

  map〈NewElement〉: (transform: Element => NewElement) => Collection〈NewElement〉

  map〈NewElement〉: (transform: Transform〈Element, NewElement〉) => Collection〈NewElement〉

  map〈NewElement〉: (Transform〈Element, NewElement〉) => Collection〈NewElement〉
}

multiline with colons

Functions
Transform〈Input, Output〉:
  Input => Output

Transform〈Input, Output〉:
  (Input) => Output

Transform〈Input, Output〉:
  (Input => Output)

Filter〈Element〉:
  (Element => Boolean, Element[]) => Element[]

Filter〈Element〉:
  (Transform〈Element, Boolean〉, Element[]) => Element[]

Map〈Input, Output〉:
  (Input => Output, Input[]) => Output[]

Map〈Input, Output〉:
  (Transform〈Input, Output〉, Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection〈Element〉 {
  filter:
    (Element => Boolean) => Collection〈Element〉

  filter:
    (Transform〈Element, Boolean〉) => Collection〈Element〉

  〈NewElement〉 map:
    (transform: Element => NewElement) => Collection〈NewElement〉

  〈NewElement〉 map:
    (transform: Transform〈Element, NewElement〉) => Collection〈NewElement〉

  〈NewElement〉 map:
    (Transform〈Element, NewElement〉) => Collection〈NewElement〉
}
Generic type arguments of methods after method name
interface Collection〈Element〉 {
  filter:
    (Element => Boolean) => Collection〈Element〉

  filter:
    (Transform〈Element, Boolean〉) => Collection〈Element〉

  map〈NewElement〉:
    (transform: Element => NewElement) => Collection〈NewElement〉

  map〈NewElement〉:
    (transform: Transform〈Element, NewElement〉) => Collection〈NewElement〉

  map〈NewElement〉:
    (Transform〈Element, NewElement〉) => Collection〈NewElement〉
}

multiline with curly brackets

Functions
Transform〈Input, Output〉 {
  Input => Output
}

Transform〈Input, Output〉 {
  (Input) => Output
}

Transform〈Input, Output〉 {
  (Input => Output)
}

Filter〈Element〉 {
  (Element => Boolean, Element[]) => Element[]
}

Filter〈Element〉 {
  (Transform〈Element, Boolean〉, Element[]) => Element[]
}

Map〈Input, Output〉 {
  (Input => Output, Input[]) => Output[]
}

Map〈Input, Output〉 {
  (Transform〈Input, Output〉, Input[]) => Output[]
}
Interface
Generic type arguments of methods before method name
interface Collection〈Element〉 {
  filter {
    (Element => Boolean) => Collection〈Element〉
  }

  filter {
    (Transform〈Element, Boolean〉) => Collection〈Element〉
  }

  〈NewElement〉 map {
    (transform: Element => NewElement) => Collection〈NewElement〉
  }

  〈NewElement〉 map {
    (transform: Transform〈Element, NewElement〉) => Collection〈NewElement〉
  }

  〈NewElement〉 map {
    (Transform〈Element, NewElement〉) => Collection〈NewElement〉
  }
}
Generic type arguments of methods after method name
interface Collection〈Element〉 {
  filter {
    (Element => Boolean) => Collection〈Element〉
  }

  filter {
    (Transform〈Element, Boolean〉) => Collection〈Element〉
  }

  map〈NewElement〉 {
    (transform: Element => NewElement) => Collection〈NewElement〉
  }

  map〈NewElement〉 {
    (transform: Transform〈Element, NewElement〉) => Collection〈NewElement〉
  }

  map〈NewElement〉 {
    (Transform〈Element, NewElement〉) => Collection〈NewElement〉
  }
}

@ericelliott
Copy link
Owner

ericelliott commented Apr 28, 2016

I love how the real angle brackets look (really, they look quite nice.. much, much better than <sharp, fake angle brackets>), but I'm afraid that typing them would be too hard.

I still appreciate the effort you put into it.

@Mouvedia
Copy link
Collaborator

If only we could use these. @ericelliott is right, it's not realistic sadly.

@tomek-he-him
Copy link
Collaborator

tomek-he-him commented Apr 29, 2016

Fair enough, so I thought actually. We do use non-ASCII characters in our team, but we all use Fedora Linux which has fantastic keyboard layouts built in.

I think Unicode in programming languages is going to flourish, but we still have to wait for efficient ways to input them.

@ericelliott
Copy link
Owner

Getting people in the US to use any non-qwerty keyboard layout is like pulling teeth, sadly. );

@tomek-he-him
Copy link
Collaborator

tomek-he-him commented Apr 30, 2016

It is a QWERTY layout. Just on steroids 😄.

https://twitter.com/tomekwi_/status/631014151033356288

@Mouvedia
Copy link
Collaborator

Mouvedia commented Apr 30, 2016

Generics are generally declared locally, inlined directly in the signature: this is what should be expected.
From what Iv read so far there's no existing convention (like a prefix to distinguish the type) that would permit us to omit that declaration.

Using generics, those of us programming in a language with a static typing discipline can recapture some measure of the freedom afforded to those using dynamically-typed languages like Python or Ruby.

After reviewing your examples I gotta reject brackets [] and parentheses (); reason being the readability and potential confusions that are all already listed.

Concerning greater/less <>, it's probably the most familiar for a decent portion of programmers. Nonetheless it has some readability problems when used in conjunction with arrow functions. I am not a big fan but I would understand if it was chosen.

Concerning whitespace, that's my first choice by elimination. It would probably need some restrictions, like enforcing lowercasing or some other distinction that would make sense—while keeping in mind #64.

@ericelliott
Copy link
Owner

It is a QWERTY layout. Just on steroids

Let me rephrase:

Getting people in the US to use any non-default keyboard layout is like pulling teeth, sadly. );

@ericelliott
Copy link
Owner

@Mouvedia

I gotta reject brackets []

I like the workarounds suggested for square bracket ambiguity:

might be resolved by writting [Number] instead of Number[]

We could write Array[T] instead of T[] in rtype. We might use [T] as short version of Array[T]. One benefit of writing the generic type in brackets is better readablity of type expressions:
(Number | String)[] vs. [Number | String] or Array[Number | String] -- maiermic

@Mouvedia
Copy link
Collaborator

Mouvedia commented Apr 30, 2016

@ericelliott I support more expressivity for arrays:

[Number, Function, String]
[Object, ...String]

// so what we currently have
Number[]
// would become
[...Number]

But that would warrant its own issue. There's no conflict between this and that: both could coexist.
IMHO generics have less use cases than advanced array types but that's irrelevant to their syntax, which is the subject of this issue.

@ericelliott
Copy link
Owner

ericelliott commented May 1, 2016

@Mouvedia

I support more expressivity for arrays:

[Number, Function, String]
[Object, ...String]

// so what we currently have
Number[]
// would become
[...Number]

I like that. I also think it would be easier to express commonly needed things, like tuples:

[Number, String]

Somewhere else we talked about using a couple symbols from regex... specifically +, *, etc... Then you could do things like this:

range(start: Number, end: Number) => Array[Number+]

Which means that the returned array will contain 1 or more numbers.

And I think Array should be the default collection type, so you can shorten that to:

range(start: Number, end: Number) => [Number+]

We should also pick either one or more or zero or more as the default, so you could potentially shorten that to:

range(start: Number, end: Number) => [Number]

e.g.,

// range(start: Number, end: Number) => [Number]
const range = (start, end) => (
  Array.from({ length: end - start + 1 }, (x, i) => i + start)
);

I believe it should be fairly easy to combine all of this syntax with the square bracket notation.

I also believe all of it should make it into the MVP.

I'd really like to hear what @Raynos has to say about this.

@Mouvedia
Copy link
Collaborator

Mouvedia commented May 1, 2016

@ericelliott check my answer @ #96. Please continue the array types discussion there.

I am totally against the usage of [] for anything but Array-like objects though.

@tomek-he-him tomek-he-him mentioned this issue May 2, 2016
@ericelliott
Copy link
Owner

@Mouvedia

I am totally against the usage of [] for anything but Array-like objects though.

Does this mean you don't like [] for generic type parameters?

@Mouvedia
Copy link
Collaborator

Mouvedia commented May 3, 2016

Does this mean you don't like [] for generic type parameters?

I consider that it will be confused with arrays—even if, in that context, that wouldn't make sense. That's partly why I favor the whitespace option: it's not a separator that can be confused with something else and with proper restrictions it won't hurt the readability.

@ericelliott
Copy link
Owner

@Mouvedia Consistency between the array syntax and the generic syntax is a feature, not an ambiguity. See this example from the array discussion:

interface Collection: Object | Array

highPass(cutoff: Number, collection: Collection[...Number]) => numbers: [...Number]

See also Generics and Collection Polymorphism from"Programming JavaScript Applications".

@ericelliott
Copy link
Owner

ericelliott commented May 4, 2016

As for your question...

I just don't know why we should favour that syntax.

Because the common case is that people will be dealing with Array collections, so it makes sense to use the [] syntax that people will find immediately familiar, and it makes sense that the default collection type is Array[], so [String] is equivalent to Array[String].

The most important design goal is familiarity to JavaScript users:

To make the signature familiar to readers, we use common JavaScript idioms such as destructuring, defaults, and rest parameters...

@Mouvedia
Copy link
Collaborator

Mouvedia commented May 4, 2016

@ericelliott

Any array syntax we adopt should use the generic syntax that we decide to go with, for internal consistency.

It's not imperative in my opinion. I consider both matters distinct. Conflating #96 with this one won't help.
For example Object[...Number] would probably be confusing: you could interpret it as an object that only has number properties (e.g. "5", "42") or none.

This issue deserves more inputs: we shouldn't rush it.

@ericelliott
Copy link
Owner

ericelliott commented May 4, 2016

It's not imperative in my opinion.

I disagree. Collection polymorphism is the most common use of generics in JavaScript. They appear in jQuery (most popular JS library ever), lodash & underscore (most depended on and 4th most depended on npm packages, respectively), collection generics exist in the JS spec itself, and nearly every JavaScript developer who's coded more than 5 minutes have dealt with generics in that context, whether they realize it or not.

I'll reiterate: The most important design goal of rtype is familiarity by reusing common JavaScript idioms. In JS, the square bracket array notation is virtually synonymous with collections.

As for letting other people chime in on it, we have literally spent months discussing it, and now we have this gorgeous issue listing all the different syntax proposals and all the pros and cons, and [] looks like a pretty good choice.

Bonus: an implementation is in the works.

It's time to call a winner. [] has my vote. Let's get a PR and move on. There are other things to discuss.

@Mouvedia
Copy link
Collaborator

Mouvedia commented May 4, 2016

@ericelliott if I didn't intervene it would have lasted for eons. I am trying to smooth things out here. Especially since it took a while to reach this point, one more week won't hurt.

Shipping what's consensual and discussing what's not that's a sound strategy.

@ericelliott
Copy link
Owner

OK, let's address the one point you did raise that has some merit. Arrays have automatically number-indexed keys. I think it's clear that when we specify a collection type with square brackets, like Foo[String], we're talking about the type of the values, not the type of the keys. 99.9% of the time this is what we want, and that syntax works fine. Let's make it easy for the 99% case.

Now, for the edge case, Objects can have typed keys. For example, how do we specify that an object's keys must all be of type symbol?

IMO, for that, we'll need some notation that allows us to address object keys, specifically, in the same way we can address object values with Foo[Bar] notation. I think discussion and suggestions for that should probably be considered a separate issue.

@Mouvedia
Copy link
Collaborator

Mouvedia commented May 4, 2016

@ericelliott

For example, how do we specify that an object's keys must all be of type symbol?

Weirdly enough @js-jslog filled an issue concerning this today: #97
I support the usage of the computed property name existing syntax.

@maiermic
Copy link
Author

maiermic commented May 4, 2016

I think discussion and suggestions for that should probably be considered a separate issue.

I created an proposal #99

@Mouvedia
Copy link
Collaborator

Mouvedia commented May 4, 2016

@ericelliott If Foo[Bar] was a shorthand of a long-version-that-makes-sense, that would be less confusing though.

@koresar
Copy link
Contributor

koresar commented May 9, 2016

Using this [] syntax how would I annotate a Promise which resolves to an array of objects?

function P() {
  return Promise.resolve([
    { name: 'John' },
    { name: 'Eric' }
  ]);
}

Let me make a best effort.

P() => Promise[...{name: String}]

Correct?

@maiermic
Copy link
Author

Spoiler: The ... syntax for generic type arguments is not yet approved. It declares an indeterminate number of type arguments.

Array takes an indeterminate number of type arguments (type of an element at the corresponding index):

Array[...T]

A short version without name Array has been proposed (for Arrays only):

[...T]

Promise takes only one formal type argument T (the type of the element it resolves to):

Promise[T]

The actual type you like to pass to the formal type argument T of Promise is Array[...{name: String}] or short [...{name: String}] which results in

Promise[Array[...{name: String}]]

or the short version

Promise[[...{name: String}]]

@koresar
Copy link
Contributor

koresar commented May 10, 2016

May I propose one more array notation? - []
So, the promise which resolves to an array would look like this: Promise[Object[]]
Or as in my example: Promise[{name: String}[]]
More intuitive to my taste.

@Mouvedia
Copy link
Collaborator

@maiermic
Copy link
Author

@koresar

May I propose one more array notation? - []

If we introduce generic rest parameters (see #101), it would be ambiguous. For example: Is Collection[] an array of collections or is it an empty collection (zero type arguments passed to generic interface Collection [...T] { ... })?

Furthermore, arrays of generic elements, e.g. Promise[String][], look quite strange/confusing (to me). Even regarding another generic syntax like angle brackets Promise<String>[] I would prefer [...Promise<String>] or Array<Promise<String>> because I find it easier to parse/grasp that it is an array of some promises and not a promise of something. That's because in case of Promise<String>[] you have to parse the hole element type (in this case Promise<String>) first until you see [] and know that it is actually an array of that type.

I admit that Promise[[...String]] or Promise[[String]] might look confusing, too, due to the double square brackets. However, it is less confusing, if you are more explicit: Promise[Array[...String]] or Promise[Array[String]].

Even though String[] might be more familiar to people familar with other languages like Java, I find [Number, Number, ...Number] obvious (in JS world), since you can create an array in a similar notation in JavaScript [1, 2, ...rest].

@koresar
Copy link
Contributor

koresar commented May 14, 2016

Perfect justification @maiermic
I'm with you on that.
Thank you

@ericelliott
Copy link
Owner

Closing in favor of Haskell-style typeclasses where syntax looks like: TypeclassName[t]. #120

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

No branches or pull requests

6 participants