Repository navigation
the typeclass model offers superior extensibility #10844
Description
Subclassing inheritance is an anti-pattern (see also this and this and this).
For those who aren't familiar with nominal typeclasses in for example Rust and Haskell, they conceptually (without getting into details and caveats yet) are a way of adding implementation to existing nominal types without modifying the source for those existing nominal types. For example, if you have an instance of existing nominal type A and your want to invoke a function that expects a nominal type B input, typeclasses conceptually enable you to declare the implementation of type B for type A and invoke the said function with the said instance of type A as input. The compiler is smart enough to automatically provide (the properties dictionary data structure or in ECMAScript the prototype chain) to the function the type B properties on the type A instance.
The has efficiency, SPOT, and DNRY advantages over the alternative of manually wrapping the instance of A in a new instance which has B type and delegate to the properties of A. Scala has implicit conversion to automate, but this doesn't eliminate all the boilerplate and solve the efficiency (and tangentially note Scala also can implement a typeclass design pattern employing implicits). This disadvantage of wrapping with new instances compared to typeclasses is especially significant when the instance is a collection (or even collection of collections) of instances (potentially heterogeneous with a union type), all of which have to be individually wrapped. Whereas, with typeclasses only the implementations for each necessary pair of (target, instance) types need to be declared, regardless of how many instances of the instance type(s) are impacted.
And afaics this typeclass model is what the prototype chain in EMCAScript provides.
When we construct a new instance, the A.prototype of the constructor function A is assigned to the instance's protoype, thus providing the initial implementation for the instance (of type A) of the properties of type A. If we want to add an implementation of constructor function B (i.e. of type B) for all instances of type A, then we can set A.prototype.prototype = B.prototype. Obviously we'd like to type check that A is already implemented for B, so we don't attempt to implement B for A thus setting B.prototype.prototype = A.prototype creating a cycle in the prototype chain.
That example was a bit stingy thus contrived, because actually we'd want a different implementation of type B for each type we apply it to. And afaics this is exactly what typeclasses model.
I am very sleepy at the moment. When I awake, I will go into more detail on this proposal and try to justify its importance, relevance to TypeScript's goals, and important problems it solves.
Note I had recently discovered in my discussions on the Rust forum in May what I believe to be a complete solution to Wadler's Expression Problem of extensibility (the O in SOLID), which requires typeclasses and first class unions (disjunctions) and intersections (conjunctions).
There are 500+ detailed comments of mine (and Keean Schupke (@keean)) over there (~335 of which are private) I need to reread and condense into what I want to say in this issue proposal. And it was to some extent and unfinished analysis that I had put on the back burner. I have elevated this priority seeing that TypeScript has the first-class unions and intersections and seeing that upcoming 2.1 is supposed to look into the nominal typing issue for #202.
I have mentioned typeclasses in a few of my recent comments on TypeScript issues.
Activity
shelby3 commentedon Sep 12, 2016
shelby3 wrote:
shelby3 wrote:
SimonMeskens commentedon Sep 12, 2016
I literally have no idea what you're talking about here, simple code sample? It sounds like you want to advocate changing an object's prototype. This is unfortunately discouraged in modern Javascript engines, as it disables a lot of optimizations. Hopefully this can be remedied in the future.
Do you want something like this?
You can already type it, though it's somewhat clunky, like this:
I agree that typescript could use a few new ways to correctly type mutating prototypes. I think my first code sample makes the most sense in that regard.
svieira commentedon Sep 13, 2016
If I understand you correctly, you want to use compile-time prototype chain injection to provide the syntactical equivalent of Scala / Rust's typeclasses? In a project using the equivalent of scalaz / cats wouldn't that generate wildly deep prototype chains?
Also, wouldn't it make interop with JavaScript as painful syntactically as working with Scala is from Java? The compiler would have to re-write all of the names of all of the methods in the prototype chain to avoid collisions, which would mean JavaScript consumers would get to call things like
someObject.$typeLookup(SomeNominalNéePrototype).someMethod()at best and at worst would have to do something likeanotherUniverse.$x1z724_q3() // SomeNominalPrototype.someMethod as of yesterday.gcnew commentedon Sep 13, 2016
Simon Meskens (@SimonMeskens) Sean Vieira (@svieira)
In hindsight the idea of Type Clasess is a very easy/obvious one, but it was not when they were first conceived. The basic problem they are focused at is extending existing types with additional functionality. As they were targeted to solve problems in a functional language (Haskell) there were no classes or objects in the sense of Java, but only pure non-hierarchical types and values. None the less, people using this language wanted a principled way to extend a type with additional functionality. They came up with the idea of specifying an interface (called a Type Class) and implementing this very interface. The difference with other, more mainstream languages is that the instance (implementation) is not carried in an implicit class, but is actually a separate dictionary (to some extent parallels can be made with Extension methods in C#). Now, wherever something is declared to depend on that dictionary (e.g. a function that uses a type of specific Type Class), the compiler rewrites both the (function) definition and the call sites. The function definition is extended to accept the dictionary as a parameter while the call-sites are rewritten using type-directed emit to pass the appropriate dictionary.
For instance, lets say we want to be able to stringify arbitrary objects. We may come up with the following Type Class:
Which in typescript terms corresponds to
As an example we can implement this Type Class for
stringandnumberPS: see https://en.wikipedia.org/wiki/Type_class
SimonMeskens commentedon Sep 13, 2016
Marin Marinov (@gcnew) We are well aware of the concept of Type Classes, we are just confused as to why editing the prototype chain would in any way facilitate writing them in Javascript. Javascript, unlike Self seems to have advocated embedding over changing delegation from the very start and modern engines like V8 have only pushed this further by focusing optimizations on embedding. If the new setPrototypeOf gets significant use, I assume this will change.
I'm not against this suggestion, as I don't quite understand what's being suggested. The theory is not the problem, I understand how type classes are useful, especially for higher kinded polymorphism. I would just like to see some examples of real Javascript code that can't be typed today and how editing the prototype chain would make this possible.
gcnew commentedon Sep 13, 2016
Simon Meskens (@SimonMeskens) Well, in this case, I want to join you as well, because I'm also confused :). What's more, I don't see how Type Classes can be implemented in TS without breaking the stated Goals/Non-goals.
SimonMeskens commentedon Sep 13, 2016
Marin Marinov (@gcnew) I also think, with Javascript being more expressive than Haskell, type classes might not be the perfect fit, like they are for Haskell. It seems that certain concepts that can only be typed as higher kinds in Haskell can be flattened and typed more directly using prototypes; on the other hand, more complex behavior can be written in Javascript, that can not be typed in Haskell (I think, my Haskell knowledge is far from deep, even though I love the language). There's a few important gaps in Typescript at the moment that are less than perfect for doing things like currying (which enables a lot of these higher kinds to become useful) though.
The concept is intriguing for sure, I just don't get the suggestion made here 😄
shelby3 commentedon Sep 13, 2016
I will soon come back to this issue to elaborate. I am catching up on other discussion (and sleep). Thank you for the curiosity and discussion.
shelby3 wrote:
shelby3 wrote:
shelby3 commentedon Sep 14, 2016
shelby3 wrote:
spion commentedon Sep 14, 2016
Please do get into more details now. If the author of the Q promise library has written a function like so:
How are you going to make this function work with a different promise library (say, Bluebird) without altering it, and without introducing a dependency between Bluebird and Q?
shelby3 commentedon Sep 15, 2016
So I had the thought that one facet which distinguishes typeclasses from subclasses is that the object of member implementations (aka properties) for the instance
thisis either orthogonal (dynamic at use site) or bound (static at declaration site) to the data respectively. With subclasses, the aforementioned implementations object dictionary is static for the life of the instance and with typeclasses that object can change depending on use site need.For JavaScript we can think of subclassing's static interfaces as the constructor function's default prototype chain.
For JavaScript we can place such implementations of the
prototypechain at will, adding or removing implementations for all existent instances of the constructor function. For a statically compiled language, the compiler can manage thisprototypechain to make sure the required implementations are presented for the need, e.g. for an instance supplied as an argument to a function which requires a specific typeclass interface. If any implementations are ever removed then employing a globalprototypechain for all instances is not thread re-entrant nor event queue (e.g. asynchronous programming) compatible. A compiler would prefer to have a separate dictionary pointer supplied on use site need orthogonal to the instance pointer, which could also be simulated with JavaScript but perhaps isn't idiomatic. But JavaScript's per-instanceprototypechain can I think suffice assuming implementations are only added and never removed.I don't know to what extent this pattern of altering the
prototypeof properties post-construction has been used in the JavaScript ecosystem?Another related issue is that type parametrized collections/containers (e.g.
ListorArray) whose element type is a heterogeneous collection of subclasses have been handled historically by subsuming to the greatest lower bound common type (which I also mentioned upthread) and relying on virtual inheritance so that all types in the collection are subsumed to a supertype of common methods and calling those methods using dynamic dispatch to call the implementation for the method of the concrete subtype. This pattern is extensible in one axis of Wadler's Expression Problem because we can add new subtypes to our collection without recompiling the source code for the supertype (interface) and without reconstructing a copy of pre-existing instance of the collection and its existing contained instances. However, it is not extensible in the other axis of the Expression Problem because we can't add a new method to the base class (supertype inteface) without recompiling the source code and reconstructing instances.By employing first-class disjunctions a.k.a. unions (and anonymous unions which are a feature of TypeScript, eliminates a lot of boiler plate) for the aforementioned container class's type parameter, the concrete types of the heterogeneous collection are not subsumed (not lost); and that heterogeneous union type is applicable to any typeclass interface need (e.g. a function argument) if there exists an implementation of that typeclass interface for every concrete type in that said union. In this way we can have extension in both axes of Wadler's Expression Problem without recompiling and reconstructing the existing instances and their sources. Solving Wadler's fundamental Expression Problem maximizes extensibility.
However, there remains a problem in that any pre-existing instance pointer for the said heterogeneous collection will be invalid if a new concrete type is added to the container's type parameter, because a reference to
number | stringis not compatible with a concrete instance of type ofnumber | string | Array<number|string>. Afaics, this problem can solved in two possible ways in lieu of cloning (but not deeply) the container. First, the container can be designed so that when a new concrete type is added, it returns a new reference to the new container type and this new reference is isolated from any external pre-existing references to container. Meaning that somehow the container's code has an internal state which tracks which issued references refer to which subsets of contained concrete types. The other solution I envision is the the compiler is smart enough to do some form of lifetimes tracking such as in Rust, and will insure that the pre-existing external references are invalidated when the new concrete type is added.SimonMeskens commentedon Sep 15, 2016
I feel like you need to write up some code samples. While I love talking type theory shop, without some real code samples, this discussion is too abstract for me. Could you create a toy sample?
shelby3 commentedon Sep 15, 2016
Sean Vieira (@svieira) wrote:
If the compiler could manage removing implementations after each use site need, then without multi-threading and asynchronous programming, we wouldn't have the problem you lament. But we need asynchronous programming, so we can't be sure the instance won't be referenced in multiple contexts simultaneously (unless we resorted to something like Rust lifetime's tracking which IMO is major pita). It seems to do this properly we need a separate implementations object dictionary for each use site (i.e. it's methods would consume an input for the instance and the instance's prototype chain wouldn't be used), then we could use the prototype chain of that object. Afaics, this would avoid your stated problem via locality and the compiler's knowledge about no conflicting names at the use site.
111 remaining items
SimonMeskens commentedon Sep 18, 2016
That's backporting emit, that doesn't count, you should show us what it does with an ES2015 target instead.
shelby3 commentedon Sep 18, 2016
Simon Meskens (@SimonMeskens) wrote:
You moved the goalposts to some arbitrary case, which is aliasing error (point sampling a different point depending on which argument you want to make). Is there only one version of JavaScript in the wild? I will repeat:
shelby3 wrote:
And I will repeat some holistic justification might be helpful to some:
In short, WTF is the point ???
Apologies for questioning what is the rationale for the orderly orderliness which is unsound and doesn't provide reliable contracts. Hopefully you can discern that I am trying to leave now as soon as you let me.
Edit: I suppose the goal is to have a trajectory that aims to look like how one would typically code in a future version of ECMAScript, except with typing hints. But for example, instead of checking the structural type to remain consistent, the community apparently decided to just trust a human to instruct (and thus potentially fool) the typing system as to what a type should be, breaking the internal consistency of the typing system while also not emitting code that checks the structural type; and checking the structural type in the absence of a nominal
Symboltag withinstanceofis what I would expect to be a more idiomatic JavaScript design pattern. I don't know what to call this sort of typing system. Apparently many adhoc vectors. I won't pretend I can wrap my mind around all the corner cases of this typing system when all its features are mixed into code. Am I missing the point? What point?RyanCavanaugh commentedon Sep 19, 2016
async/awaitis indeed the only place where type-directed emit occurs. This is only considered OK because a) this is a huge value add, perhaps the highest emit value prop in the language and b) we can be sure that the async types required do not require loading other user-code files. Nothing else has met that bar and probably nothing else will.RyanCavanaugh commentedon Sep 19, 2016
This thread has gotten too long for anyone to read in a reasonable length of time, and the proposal under discussion has shifted substantially. It seems like we've elucidated some key constraints in TypeScript and if anyone wants to take this idea to a place where we could actually emit it, that'd be welcomed in a new issue. Closing this one (note that closing does not prevent new comments and people are welcomed to continue discussion here if needed in order to reach proper clarity for a new proposal).
SimonMeskens commentedon Sep 19, 2016
Yeah, I'd gather up a proposal from this thread, but I don't see how type classes would work without polymorphic functions. Maybe we can come back to this if we ever get extension methods, maybe type classes are the way to go about extension methods. Not sure.
shelby3 commentedon Sep 23, 2016
You may find interesting or useful this summary of variance issues w.r.t. to subclassing, typeclass objects, and my new union solution to the Expression Problem.
dead-claudia commentedon Sep 23, 2016
shelby3 I'd like to clarify that the private fields proposal is
theoretically transpilable (with acceptable performance) to ES6, but it can
only be accomplished with WeakMaps, and thus can't go down to ES5.1 without
native weak bindings (like node-weak in Node or java.util.WeakHashMap in
Rhino or Nashorn).
On Sat, Sep 17, 2016, 22:12 shelby3 [email protected] wrote:
kitsonk commentedon Sep 23, 2016
Note that WeakMap can be fully polyfilled in ES5. I still think that the proposed private fields could be fully down emitted to ES5.
dead-claudia commentedon Sep 23, 2016
Kitson Kelly (@kitsonk) I was mainly referring to the memory characteristics. (Using a
pair of backing arrays is technically spec conforming, although it's highly
memory inefficientc.) And the existing userland "polyfill" that is mostly
weak is not truly weak IIRC.
On Fri, Sep 23, 2016, 05:19 Kitson Kelly [email protected] wrote:
kitsonk commentedon Sep 23, 2016
I don't believe the core-js polyfill is leaky (though there maybe a few edge cases... I know there were some changes to deal with frozen/sealed keys which would leak). Actually one of the changes of the spec, before it was final (removal of
WeakMap.prototype.clear()) was specifically to make it possible to make it polyfillable with similar GC features to native.dead-claudia commentedon Sep 23, 2016
Kitson Kelly (@kitsonk) You're right. I was wrong. (I had to read the source.)
So apparently, the ES private fields proposal is transpilable down to ES5, complete with weak semantics, just using a WeakMap polyfill if necessary.
(I'm the one who pointed out in the proposal's repo before it was migrated to TC39 that its semantics could actually be transpiled to ES6 using WeakMaps, which is why I commented in the first place. The proposed spec change itself uses WeakMaps to specify it, although engines really shouldn't actually use those to implement it.)
shelby3 commentedon Sep 24, 2016
The original idea of this thread, becomes the fast track hack to get typeclasses into TypeScript via my proposal to have ZenScript transpile to TypeScript. Thoughts?
masaeedu commentedon Oct 25, 2016
With much respect to all involved, this thread is far too long and contains far too many edits and digressions to be able to parse out a proposal for type classes. shelby3 or someone else who is the primary proponent of this, would you be so kind as to post a new issue with a concrete proposal distilled from this discussion?