Open ghost opened 11 years ago
@kitcambridge thanks for your thoughtful and considered questions. see my replies inline below.
would it be accurate to think of custom types as interfaces, in the Java/C# sense?
Similar, but not in the sense of concrete implementation. Rather, it's structural typing all the way down. It's closer to the concept of an interface in TypeScript or Go. I'm not familiar with ActionScript, but I'm told it's similar. In just JavaScript terms, this is like the idea of "Array-like" being used to describe an object with a {length: Number}
property and numeric indices. The important thing is that it's just describing the structural attributes of a Value.
given that there's an existing syntax for expressing multiple types (
|
), do you think there's value in reusing that instead of having the comma serve as a delimiter for multiple specific types?// A possible alternative to
Generic<Type1, Type2>
. Generic<Type1 | Type2>
Generic<Type1|Type2>
is valid under the proposal to mean "Generic is a type structure which varies by another type which may be either a Type1 or a Type2", while Generic2<Type1, Type2>
means "Generic2 is a type structure which varies by two other types, one of which is a Type1 and the other of which is a Type2". (See also: covarianceMost common cases only vary by a single type (such as Callback<User>
referenced in the spec). Given that jsig is meant to be read by humans, it would be worth discussing whether multivariate generics hinder or improve communication and comprehension. The most common usage I've seen in other language for multivariate generics is in describing functions, but in jsig we use ES6 lambda (arrow function) notation instead (for example, (Number, String) => Boolean
could be described as Function<Number, String, Boolean>`).
Similarly, I presume that a custom or compound type can also be used as a specific type?
// An array of
Socket
objects. Array<ReadStream & {write: Function, connect: Function}>// ...Though that would probably be better expressed with a custom type. Socket: ReadStream & {write: Function, connect: Function} Array
Exactly. The decision of which to use is stylistic and depends on the amount of context you want to establish in your documentation. A library, platform, or framework, for example, could adopt a few custom types which it uses in many places for the sake of clarity at the tradeoff of increased context that the reader has to hold in their head. On the other hand, if a single function you're writing has a particular constraint, it may be more clear to express it as a compound type. A programming analogy might be point-free style vs assignment, with similar readability tradeoffs.
Return Types: The emphasis on return types makes sense (especially in the context of your "Thinking in Promises" post), but, given the popularity of callback-based patterns (where the return values aren't necessarily meaningful), using
void
to signify "no value" feels a bit verbose. Would it be ambiguous to simply omit the type in this case? (e.g.,(param1, param2) =>
).
I've been turning this one over in my head as well. In my personal style I hate not returning (in fact, I like languages which return the value of the last expression in the function body by default, such as F# or CoffeeScript). However, for the purpose of JavaScript, where a sizeable part of the community writes in a style which de-emphasizes return values, it makes sense for jsig to have a good story for this use case. Do you have ideas for a clearer syntax? (Value) =>
doesn't sit well with me - an arrow pointing at nothing feels incomplete and ambiguous. My main issue with (Value) => void
being inadequate for this use case is not that it's too long, but that it's artificially specific. The function is actually making no claims as to it's return value one way or the other, but this jsig expression is. The .Net framework has an interface called Action
. From their docs, Action "Encapsulates a method and does not return a value.". We could support something similar using jsig Generic Types, but I don't want to create a "base interface prelude" for jsig if at all possible. I want jsig to be immediately approachable without context, but with a clear specification as reference material in case a particular notation is not clear. Your thoughts on alternatives are welcome.
Parameters: The symmetry with the ES 6 fat arrow and rest parameter syntax is wonderful, and I also like the ability to specify optional parameter names. This might be too special-case for
jsig
, but what do you think about mirroring the spread syntax for an arbitrary number of parameters of different types? As an example, consider this definition forFunction#bind
:bind(thisArg: Object, [...parameters]) => Function
My intention was for this case to be covered by bind(thisArg: Object, ...parameters:Value) => Function
- that is, each of the parameters is a Value, but not necessarily of the same specific type. The spec is rather hastily drafted - I welcome pull requests for language clarifications.
But further, what do you think about making types in function parameter lists optional? That is, if a function parameter is named but there is no type specified, then the notation is simply not making any claims or constraints about its type. This would make jsig more flexible, so that types could be states unambiguously where it matters and left unconstrained where it doesn't:
bind(thisArg: Object, ...parameters) => Function
It seems like this would be much more convenient for expressing these kinds of higher order function signatures where they don't much care about the types of lower level functions. (Someone with a stronger PLT would walk all over my last sentence, but I guess I'm referring to the amount that a programmer reading the type annotation would have to care, not a type checking compiler looking for provable safety guarantees)
Again, Kit, thanks for your comments. If you know of anyone else you think might be interested, please share - I'd love to start a broader dialog around this and see what people's needs are. In the mean time, I'm updating the documentation of some of my npm modules to adopt jsig.
I've been turning this one over in my head as well. In my personal style I hate not returning (in fact, I like languages which return the value of the last expression in the function body by default, such as F# or CoffeeScript). However, for the purpose of JavaScript, where a sizeable part of the community writes in a style which de-emphasizes return values, it makes sense for jsig to have a good story for this use case. Do you have ideas for a clearer syntax?
So, I've been constructing some notes about your ideas @jden and this is the issue that initially felt strange when I read through the proposal. My uncertainty arises from the fact that void
is--as you said--an artificial construct of jsig
without inherit meaning in the described language.
Based on the comments and intent of jsig
so far, it's clear to me the focus is on human readability...which, as a fellow human, I can appreciate. With that pragmatic approach in mind, it seems reasonable to me that the documented return value of a function without an explicit return is undefined
such that:
(param1, param2) => undefined
Unfortunately, this approach is more verbose but I would also argue that it communicates intent in a more human-friendly way...particularly through the lens of expectation in assignment.
For callback based functions I annotate them like
readFile :: uri:String -> cb:Callback<Error, Buffer> -> void
This is reasonably readable and expresses the important point that the function returns no value. Using something like cb:(Error -> Buffer -> void)
get's really messy.
I've personally been playing with a haskell style type documentation recently ( https://github.com/Raynos/immutable-hash#documentation ) and I definitely like the ability to write single line comments above my functions documenting the types. This is a massive improvement over verbose multiline jsdoc statements
@Raynos's suggestion would be helpful for functions that give a callback and/or a return value i.e. with if(cb){cb(null,blah)}else{return blah;}
Thank you so much for putting this spec together, @jden! I've seen various type annotation proposals for JS, and generally found them ill-suited to the language. Reading through the (now-defunct) ECMAScript 4 spec convinced me that attempting to integrate an existing type system would end in disaster. To that end, I'm excited about
jsig
because I think it's the first proposal that embraces JavaScript's dynamic nature.My favorite part of
jsig
's design goals is that it's intended for humans: it's sufficiently descriptive for expressing intent ("a vocabulary for explicitly stating expectations"), but avoids the ceremonial boilerplate that plagues statically-typed languages. I also fully agree with your criticisms of JSDoc. Once you progress beyond built-in types and constructors, its utility becomes limited. JSDoc's@type
annotations work well for "classical" (read: Java-inspired) patterns, but fall significantly short of meaningfully describing contemporary JS patterns.Basic and Custom Types: I'm quite fond of using dot notation and the familiar object literal-style syntax (and array literal syntax, in the case of tuples) to describe objects and custom types. A brief clarification question: would it be accurate to think of custom types as interfaces, in the Java/C# sense?
Generic Types: Something about the use of angle brackets irked me at first (perhaps due to its association with Java generics, or, more likely, because I viscerally associate angle brackets with HTML), but I've gradually warmed to them. One question: given that there's an existing syntax for expressing multiple types (
|
), do you think there's value in reusing that instead of having the comma serve as a delimiter for multiple specific types?Similarly, I presume that a custom or compound type can also be used as a specific type?
Compound Types: I love that this isn't restricted to just describing inheritance patterns: it works equally well for functional mix-ins and prototypal inheritance, to name just two contrasting philosophies. The choice of the ampersand is appropriate and reads well, in line with
jsig
's philosophy.Return Types: The emphasis on return types makes sense (especially in the context of your "Thinking in Promises" post), but, given the popularity of callback-based patterns (where the return values aren't necessarily meaningful), using
void
to signify "no value" feels a bit verbose. Would it be ambiguous to simply omit the type in this case? (e.g.,(param1, param2) =>
). We already have the optional parameter syntax (?
) for optional parameter values (or explicitlyundefined
values; I believe ES 6 treats them interchangeably for default parameters)—so I don't think there's a need to keepvoid
around for that.Parameters: The symmetry with the ES 6 fat arrow and rest parameter syntax is wonderful, and I also like the ability to specify optional parameter names. This might be too special-case for
jsig
, but what do you think about mirroring the spread syntax for an arbitrary number of parameters of different types? As an example, consider this definition forFunction#bind
:Whew...this turned out to be quite a lengthy response. In short, I'm excited about
jsig
—both its syntax and its philosophy—and looking forward to hearing your feedback!