-
Notifications
You must be signed in to change notification settings - Fork 38
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 #55
Comments
So you want the signature of |
Yes, whereby the map function that I described takes the array as second parameter. |
IMO, if you put too much inline, it becomes hard to read. I suggest splitting it: Transform(a: Any) => b: Any
Map(fn: Transform, arr: []) => newArray: [] |
Since
|
Yep, but my |
intentionally generic means intentionally error-prone. It's like a leak in the type system. Everything that depends on the undefined type variable is untyped and can not be analyzed. I already mentioned two examples of other type systems. TypeScript uses generics, but I'm not sure if it is the way we should go. I really like the way of functional type systems (simple, clear and expressive), but I'm not sure how compatible it is with the existing rtype system. I mentioned those examples, since TypeScript and Haskell are mentioned in About Rtype:
|
@maiermic I think you missunderstood @ericelliott's comment.
Meaning he supplied a generic function, You can supply a typed function. Transform(a: String) => b: String
Map(fn: Transform, arr: [String]) => newArray: [String] |
👍
|
@maiermic It's also worth noting that Haskell is polymorphic and generic programming is common in Haskell. https://wiki.haskell.org/Generics |
More to your original point: currently, we don't support type variables like TypeScript's generics in function signatures, and the TypeScript syntax feels like an assault on my senses. I'm open to the idea of adding this feature, if we can come up with syntax that doesn't make me want to claw out my eyes. I'll repeat my invitation for you to help:
|
It's also worth noting that just because a type isn't in a signature does not mean that the variable is untyped, and the Haskell docs actually show a map example to demonstrate:
For the expression map ord the type checker finds out that the type variable a must be bound to the type Char and b must be bound to Int and thus it concludes:
IMO, type inference should be favored as much as possible. Trying annotate every little thing can add a lot of noise. |
type inference 👍
Maybe I still mispreceive it. Let's try to clear up misunderstandings: Transform(a: Any) => b: Any
Map(fn: Transform, arr: [Any]) => newArray: [Any] Now someone else likes to use this function like that:
In case a Transform(a: String) => b: Number
Map(fn: Transform, arr: [String]) => newArray: [Number] In case b Transform(a: Number) => b: Boolean
Map(fn: Transform, arr: [Number]) => newArray: [Boolean] In case c Transform(a: String) => b: String
Map(fn: Transform, arr: [Boolean]) => newArray: [Number] Since Transform(a: Any) => b: Any
Map(fn: Transform, arr: [Any]) => newArray: [Any] Thus, the type conflict in case c is not recognized. Who should supply a typed function? Author or user? Where and how should it be supplied? It would be an imposition and error-prone for the user to add a concrete type signature before each usage of
TypeScript uses generics to define type variables
You can pass type variables to other gerneric type definitions:
You don't need to specify generic types on each call just in the definition. Generics are widespread and established, but they are considered to be quite complex.
What do you not like? Maybe we can find a better solution, if you describe your feelings in more detail. By the way, we already use generics under the hood to describe arrays, don't we? |
Yes, and I even considered TypeScript's generic syntax for that purpose, but 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). Are there any other generic syntaxes we could use as inspiration? |
👎 For angle brackets |
Scala uses square brackets: def map[T,U](fn: T => U, list: List[T]): List[U] = ... We could write |
👎 for the long version |
But I would say that is even more confusing 😉 We could use another name
or
Whereby the short syntax is more readable, but less explicit (special case)
We characterize the naming conventions. That's why should think carefully about naming predefined types. Some more examples of generic types (not considering one of these naming conventions):
|
I like the square bracket syntax MUCH BETTER. I'm OK with this: Stream[T]
Map[K, V]
Object[K, V] // Object that is used like a map (might be controversal, but is common in JS world)
Pair[A, B] |
I don't think we should reuse square brackets since in JS it's for arrays.
|
Wouldn’t square brackets mentally collide with our syntax for typed arrays? Elm just uses spaces for that. Similarly to Haskell, it has lowercase for generics and uppercase for concrete types: Stream t
Map k v
Object k v
Pair a b Transform t u: (x: t) => u
Map t u: (fn: Transform t u, list: t[]) => u[] |
This is my favorite suggestion so far. 👍 @tomekwi Advantages:
|
Woohoo 🎉! Let’s wait what the others say. |
@tomekwi love your suggestion. I'm a fan of the Hindley-Milner type system. (https://en.m.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_system) |
Whooah, looks hardcore! 😉 |
So simple yet so clear. 👍 on space |
The following looks very bright and logical. Although, rather unreadable. Let me explain. Stream t
Map k v
Object k v
Pair a b
Transform t u: (x: t) => u
Map t u: (fn: Transform t u, list: t[]) => u[] 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). 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: I would recommend to level the readability up to the highest level possible, gentlemen. Haskell is still not readable unfortunately. TypeScript syntax is readable to most developers. They would easily understand that: Transform(a: T) => U
Map<T, U>(fn: Transform<T, U>, list: T[]) => U[] But not many would understand this (hopefully I understood it correctly): Transform t u: (x: t) => u
Map t u: (fn: Transform t u, list: t[]) => u[] C++/Java/C#/TypeScript languages have so called "generics". I'd recommend utilizing it's syntax - i.e. chevrons |
Let me list "arguments" against the angle brackets you have expressed:
Do you feel like those are far from being an argument? :) |
Converted by force? |
Those "converted" people are also our community. |
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)` This case is a common one. It could be bypassed with a function alias
but you don't always like to do that. To be fair, there may be awkward examples in other syntaxes, too. Please comment if anything comes into your mind. |
That makes sense. I meant that could be expressed by generics like this: (message?: a, ...optionalParams: b[]) => Void – but on second thoughts, it only makes the thing more obscure. Generics only really make sense in |
and
are not the same. The function ( What should be the type of
I did not pass a value of every existing type, but it would be possible. Hence, |
Definitely 👍 |
Ah, I think I see your point:
May be interpreted as: MyInterface(String) => String Of course, if MyInterface(String => String) Looks like there are ambiguity and readability problems with all of the syntax proposals so far. Which one has the best readability + disambiguation solution? |
No, it isn't, but
because of the higher binding of an interface declaration (I should have mentioned that earlier 😞 ), which results in
There is no such ambiguity.
is a different type. It is a concrete type. It is not an interface declaration, because it would be missing an interface body. |
Ah, yes, thanks for the reminder. In that case, I think I'm happy with the function call syntax. Are you? |
Even Eric is confused. Think of that. Every one here got confused on how the new "function call" syntax actually works. Add several other confusions we had above. And imagine thousands of other people being confused. Surprisingly, none in this thread got confused with the "angel brackets" syntax. |
@koresar Maybe. Keep in mind that none of us are familiar with the existing syntax, and we are familiar with other type system syntax, so it's easy for our brains to jump to more familiar constructs to try to parse proposal examples. I think being confused about these examples says something, but I think that a little familiarity will go a long way toward reducing confusion in the future, whereas being familiar with angle bracket syntax with inline signature annotations more than a decade has never made them much easier for me to parse when they're intermingled, as in C++ templates... So I'd really like to avoid that. To reiterate, what really confuses my brain about angle bracket syntax is when it's mixed with function signatures, as in the type constructor declarations. For the purposes of the signature demonstration, I'll return to the map function example rather than the map data type example: 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. I strongly prefer this version for declaration. Because the syntax noise from the angle brackets is gone, it lets the type parameters take a supporting role, rather than making them the noisiest part of the signature: map t u (fn: (x: t) => u, list: t[]) => u[] That allows my brain to much more easily focus on the important parts of the signature: the positions of the types within the signature body itself. This is more important than it might seem at first, I think. Imagine you're driving a car, and there's some really important information in the dashboard about your engine overheating, warning you that the car's RPMs are 3x higher than the safe level. The trouble is, it's not flashing. It's not red. There's no motion drawing your eye to it. You're driving a fancy new car with a GIGANTIC display off to the right. Normally that big display is showing you something innocuous like the name of the current track playing on your car stereo but right now it's flashing a bright red cryptic message at you -- you may be distracted and not notice the critical information tucked away in the dashboard dials. In my brain, angle brackets mixed into function signatures has that effect. It draws my eyes and my brain's attention away from the signature itself, and to the angle brackets -- not even the contents of the angle brackets, but the brackets themselves. The thing is, in the context of the function signature, I don't believe that the brackets need to be there in the first place. In fact, I really like this form we discussed earlier: map t u (fn: (x: t) => u, list: t[]) => u[] So now that we've established that the space separated style might not work as well for invocation, I still want to have my cake and eat it, too. Does it make sense to mix and match spaces for declarations and angle brackets for invocation? map t u (fn: t => u, list: t[]) => u[]
interface mapStringsToStrings:
map<String, String> As @maiermic pointed out, part of the visual confusion still exists because you can pass an arrow function signature as a concrete type: interface StringStringThing:
MyInterface<String => String> Which visually looks a bit like this: interface StringStringThing:
MyInterface<String => // wtf is the rest of that nonsense? And then your brain registers that there's an Which is why I like this version: interface StringStringThing:
MyInterface(String => String) I was only worried for a moment because I thought it may be confused with this signature declaration: MyInterface(String) => String But in order to interpret // Error: Unexpected token, `:`
MyInterface:(String => String)
^ Conclusion & Proposal
Questions:
|
P.S. "Even Eric is confused." This happens a lot, and should not be taken as evidence of anything particularly interesting. ;) |
Looks like our brains work in a different ways. To make the best decision we need to survey other people in order to understand how the majority of the developers' brains work. Could you create a poll with a single question "What's more readable to you?" and three examples of the proposed syntax (space delimited, function-like, and angle brackets)? |
I already started working on a survey of generic syntaxes. I'd like to give an overview of each syntax with description and examples. Further, I try to point out advantages and disadvantages as objective as possible. Sadly, I'm really busy in the next weeks. Thus, it may take some time. Please be patient. The examples are intended to cover all basic use cases of generics: functions, objects, methods (with additional generic types that depend on the parameters and are not defined on the object interface itself), considering type restrictions/bounds and declaration-site variance annotations. Am I missing something? They should be preferably familiar and not unnecessary complex. I'm open for suggestions. I miss good examples for type restrictions/bounds and declaration-site variance annotations yet. Any ideas? |
If you do that just select an use case that is common for the examples. |
Given a generic interface (here written in function/parentheses syntax): interface Collection(T) {
// ...
} How can we check the generic type at runtime? import rtype from 'rtype';
// I don't know how type checking will look like. Thus, I define it like this:
/**
* Returns `true` if `a` is of type `t`, else `false`.
* (t: Rtype, a: Any) => Boolean
*/
const isOfType = rtype.isOfType;
// Collection(Any) => Void
function (c) {
if (isOfType(Collection(Number), c)) {
// ...
} else {
// ...
}
} In other words: How can we implement reified generics? Note: Syntax does not really matter in this case. |
@maiermic, my first-pass naive plan is essentially this: Generics are parameterized functions which get compiled to concrete types based on the type parameters passed to them. Here's a naive first pass at the runtime compiler output for each type: interface TypeChecker {
interfaceName: String
interfaceString: InterfaceString, // original, unmodified interface string here
isType?: Predicate, // For non-function types, a synchronous predicate returns true if input matches interface shape, false otherwise
wrapper: WrapperFunction // for functions, the wrapper optionally monitors arguments, return values, throws, and warns if they deviate from the described interface
}; For generics, the type parameter information could be included in the TypeChecker interface, but I thought it would be enough to fix them inside If you see problems with that approach, I'd love to hear them. See also #62 |
How does the corresponding JavaScript function |
A runtime check would iterate through the Array and check that every element satisfies the Is it possible to combine compile time static analysis & runtime analysis to compile optimized runtime checking strategies? |
How about empty arrays? Pseudocode: var a: String[] = [];
if (a is Number[]) {
a.push(666); // `Number` is inserted into `String[]`
} |
If there are no elements, then "every element" satisfies the P.S. I don't think I understood what the pseudocode was meant to demonstrate, which means I may have completely misunderstood your last comment. |
Sorry, I am short of time, but I try to explain the issue more clearly: |
Ah, I see what you mean. Yes, |
Static analysis is not able to catch the error 😟 Runtime checks are only used if static analysis does not provide (enough) detailed type information (for example type is |
Further discussion can be found in the RE: generic types syntax can be found in #80. |
How do I type a function with a type variable in rtype?
Example: map function
In functional languages you write something like this:
In TypeScript you use generics (ramda.map):
The text was updated successfully, but these errors were encountered: