-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
the typeclass model offers superior extensibility #10844
Comments
|
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? class A <T> extends <T> {} You can already type it, though it's somewhat clunky, like this: class A {
someProperty: string
}
class B {
otherProperty: string
}
interface IC {
newProperty: string
}
class C extends A {
newProperty: string
}
let c: A = new C();
Object.setPrototypeOf(c, new B());
let newC: B & IC = <B & IC>c; 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. |
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 |
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: class Stringify a where
stringify :: a -> String Which in typescript terms corresponds to type Stringify<a> = { stringify: (x: a) => string } As an example we can implement this Type Class for instance Stringify Int where
stringify = show
instance Stringify String where
stringify = id
-- should be read as, a function accepting a value of generic type `a`
-- (which is also Stringify-able) that returns a string
appendX :: (Stringify a) => a -> String
appendX x = (stringify x) ++ "X"
example = appendX (4::Int) ++ appendX "2" const StringifyNumber: Stringify<number> = { stringify: x => String(x) }
const StringifyString: Stringify<string> = { stringify: x => x }
function appendX<a>(dict: Stringify<a>, x: a) {
return dict.stringify(x) + "X";
}
const example = appendX(StringifyNumber, 4) + appendX(StringifyString, '2') |
@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. |
@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. |
@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 😄 |
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.
|
|
Please do get into more details now. If the author of the Q promise library has written a function like so: function doSomethingWithPromises(p:NominalTypeImplementedByQPromise) { ... } 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? |
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 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 I don't know to what extent this pattern of altering the Another related issue is that type parametrized collections/containers (e.g. 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 |
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? |
@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. |
@SimonMeskens wrote:
I'll need to review the discussion I had at the Rust forum as we discussed where the HKT and HRT (higher-ranked types) apply. Note TypeScript's new |
So again per my reply to @svieira, if we've implemented a hypothetical typeclass interface for a specific class, we can't attach that implementation to the So the methods of our implementations will not refer to the So let's write some hypothetical code presuming that we will reuse TypeScript's trait Foo {
foo(_: number): string
}
trait Bar {
bar(_: string): number
}
class C {
constructor(public value: number) {}
}
implements C for Foo, Bar {
foo(x: number) { return new String(x + this.value) }
bar(x:string) { return new Number(x) + this.value }
}
function foo(x: Foo) { return x.foo(0) }
function fooBar(x: Foo|Bar) { return x.foo(x.bar('1')) }
foo(new C(0))
fooBar(new C(1)) The emitted JavaScript will include (assuming const C_Foo = { foo(_this, x) { return new String(x + _this.value) }
const C_Bar = { bar(_this, x) { return new Number(x) + _this.value } }
function foo(x, C_Foo) { return C_Foo.foo(x, 0) }
function fooBar(x, C_Foo, C_Bar) { return C_Foo.foo(C_Bar.bar(x, '1')) }
foo(new C(0), C_Foo)
fooBar(new C(1), C_Foo, C_Bar) Is anyone using a pattern like this in their JavaScript code already? |
Why do the emitted People might be doing this already by writing code like this: interface Foo {
foo(_: number): string
}
interface Bar {
bar(_: string): number
}
class C {
constructor(public value: number) {}
}
interface C extends Foo, Bar { }
// Could use Object.assign in ES6 here if we wanted to.
// In TypeScript 2.0, these functions would have `this: C` as their first parameter
C.prototype.foo = function(x) {
return x + this.value; // n.b. Don't ever use new String / new Number!
}
C.prototype.bar = function(x) {
return +(x + this.value);
}
function foo(x: Foo) { return x.foo(0) }
function fooBar(x: Foo & Bar) { return x.foo(x.bar('ok')) }
foo(new C(0))
fooBar(new C(1)) |
@RyanCavanaugh wrote:
Because the instances of
Typo. Fixed. Thanks.
That requires copying all the methods of each implementation into the instance every time the implementations are used. What if you have 100 methods on the implementations. Very costly to performance. And you have to remove them from the instance when you are done using them at the use site of the functions |
What I'm saying is, this code unconditionally crashes, so I can't reason about what your intent with it is. The parameter |
@RyanCavanaugh apologies it is 7am and I haven't slept since yesterday morning. I fixed the typo. Thanks. Also please re-read my prior comment as I explained why your code sample is not equivalent. |
As @RyanCavanaugh points out, I don't see how you add anything new to the language at all with that example. People are already writing that exact functionality by embedding onto the prototype of the constructor. There's a hint of extension methods going on, something that has been discussed and will be added depending on certain TC39 decisions. The only thing I can see that's missing so far is this behavior: class A <T> extends T {} Which would add a type-class-like functionality that lets us type changing prototypes more concisely. |
@SimonMeskens wrote:
Re-read my reply to @RyanCavanaugh. I explained why his code is not equivalent. |
I didn't say it was equivalent, I said your example adds no new functionality to the language imo, except for a weak version of already proposed extension methods. |
@shelby3 please stop editing your comments to add substantially new information to them. It's extremely confusing. You write pages and pages of text, then edit them to forward-reference comments ahead of them (which makes the thread impossible to read top-down). And now I have to re-read every comment every time I go to write something to make sure you haven't addressed something new in an edit since the last time I left my own comment. It also makes it look like I'm pointing out things which are no longer true (or failing to address new objections which were edited in since I wrote my comment), which makes the thread more confusing to people reading it the first time through. |
@SimonMeskens wrote:
Please show me how you can fix the problems I outlined to @RyanCavanaugh with the existing features of TypeScript. @RyanCavanaugh wrote:
That is an unreasonable request. Sometimes that is not possible to adhere to. Please be tolerant.
I acknowledged the typos you discovered. Maybe Github can add a changes history for comments some day, similar to Stackoverflow has for Q & A and Steemit.com has every change is recorded on a public blockchain. I will not spam comments to simulate a missing change history feature, because the priority for readers is primarily comprehension and not blame. Github is showing you how long ago each comment was edited, so you can determine which ones you need to re-read. @SimonMeskens also edited his comment and added this:
|
You can't, because what your code does comes down to extension methods and those are currently in TC39 limbo. If they are accepted, then yes, you can do what your code does and there's already several proposals on how to make Typescript type those correctly. If they are rejected by TC39, then there probably won't be extension methods in TypeScript due to the stated goals of the project. In which way does your proposal change that situation? |
Please go ahead and provide an example 😀 |
Almost every word you wrote there is incorrect @shelby3. You're not holding a mirror up, you're showing the world just how correct I was. Let me explain it to you in simple terms a layman would understand: JavaScript has differential inheritance, it achieves this by storing a reference to a delegation object. Any objects that share the same delegation object are of the same type. This delegation object can be checked at runtime, hence type can be checked at runtime. This is an extremely common pattern in real world JavaScript. As for TypeScript not being a superset, how is it not? Every valid piece of JavaScript is valid TypeScript and compiling any code that is valid JavaScript results in the compiled output being exactly the same as the input. How is that not a superset? |
@SimonMeskens wrote:
Hyperbole.
I am also not blind in both eyes.
I have a different definition of the utility of a superset. I can't take any TypeScript code and run it on JavaScript without compiling. It is a useless point that TypeScript can compile JavaScript, because I don't need TypeScript for doing nothing at all. I think the point you are wanting to make is that the correspondence between TS and JS is tight and the learning curve is slight and gradual. Nevertheless, they are different languages. |
A is superset of B means that any B is valid A, but not every A is valid B. The fact that any B compiles to the exact same B proves this without doubt. This is the case for TypeScript and JavaScript. Also, @spion called you out on an incorrect statement, I dare you to provide the example he asked (hint, you won't find it). As for the type/uni-type discussion, I don't want to get super deep into it, but for one, Anyway, I'm not sure how useful the definition in the article you linked is, in the discussion we were having, it's apparently a semantics debate, which, ugh, I don't want to get into. The point is, if JavaScript doesn't have types, then TypeScript doesn't check types, which I'm fine conceding. Replace every instance of the word type with contracts and same discussion. TypeScript checks contracts of prototypes of objects at compile time, which you can also check in JavaScript at run-time. It also checks structure of objects, which again, you can also check at run-time. Both of these are guaranteed. So basically, whatever TypeScript checks at compile-time, whatever you want to call it, is also checkable at runtime. This means that there's no erasure of that "whatever" either, as you claim. |
@RyanCavanaugh wrote:
Unfortunately TypeScript does have an example of this. I have raised issues about this before (#6007 and elsewhere), but so far this exception remains. You can do this in the latest nightly: // @target: ES5
declare class Promise<T> { then(cb: (val) => any); }
declare class MyClass extends Promise<any> { }
const foo = async () => 42;
const bar = async (): MyClass => 42; which emits: /*...helpers omitted for brevity...*/
var foo = function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) {
return [2 /*return*/, 42];
}); }); };
var bar = function () { return __awaiter(_this, void 0, MyClass, function () { return __generator(this, function (_a) {
return [2 /*return*/, 42];
}); }); }; Note that [EDIT: removed quotes from @spion and @SimonMeskens that refered to something else] |
Since this issue also reflects on TypeScript's structural typing, I'd like add to my last comment another interesting consequence of the unique way async function typing works. When targeting ES6, the return type annotation of an async function is nominally, not structurally typed. That is unlike anything else in the language (i.e. function decls/exprs, arrow declr/exprs and generator functions are all structurally typed). For example: // @target: ES6
// Both functions are identical in type and runtime behaviour.
function f1() { return Promise.resolve(42); } // OK: f1 is () => Promise<number>
async function f2() { return 42 }; // OK: f2 is () => Promise<number>
// Same functions, now with return type annotations.
// For the ordinary function f3, the return type annotation is structurally typed.
// For the async function f4, the return type annotation is nominally typed.
function f3(): {then} { return Promise.resolve(42); } // OK: f3 is () => {then: any}
async function f4(): {then} { return 42 }; // ERROR: annotation must be Promise<T> In ES6 an async function's return type annotation must refer nomimally to the global |
Yortus, here's the original quote:
Your example is indeed weird, but not a cast. |
@SimonMeskens it is a counterexample of @RyanCavanaugh's quote. EDIT: I'll remove your and @spion's quotes in my comment for accuracy sake. |
You quoted @spion and I, we both were not talking about @RyanCavanaugh's quote. |
@SimonMeskens fixed thanks |
Agreed, but I am saying that I fail to see the utility. I think if it were to have any utility instead we want a supertype aka subset, where every TypeScript would run uncompiled on every JavaScript otherwise I don't really see what utility it provides to have that attribute. The moment I avail of any feature in the superset which is not in the subset of JavaScript, my code no longer runs on JavaScript without compilation by TypeScript. As I wrote in my prior comment, I understand supersetting might ease transition since once could start from JS and add only the superset feature they wish to use. I have never argued against having a tight correspondence to JS, but nevertheless we do have to add features to TS else it is a useless endeavor. Have I ever advocated removing the superset capability which types JS as
I need to sleep sometimes. Patience.
No the point is it doesn't just have a nominal type undifferentiated from You've not acknowledged my points:
The point is that the types can change at run-time and thus there is no typing (only a uni-type of Without the ability to reason about times at compile-time, the dynamic typing is less useful. What do I do when my code expect a type T and instead is dealing with a type Q. Throwing run-time exceptions is the antithesis of safe code, because there is no way to test all possible code paths at run-time due to the Halting Problem.
That doesn't cover structural types. And it doesn't cover the fact that my types may include objects which reference objects and thus looking at one small fragment of my mental type may be aliasing error, i.e. encapsulation isn't enforced.
It can't because it doesn't enforce that the types don't change. This is just heuristics akin to sticking our finger in the air and guessing which way the stock market will go today. As the Complexity of Whoops (compile-time contracts broken) increases, the utility of that heuristic becomes closer to useless and even can become harmful as it is making contracts which are so frequently broken. |
First let's establish that TypeScript does emit code from typing judgements which is not verbatim the same code that was input to the compiler with types elided. abstract class Foo {}
abstract class Bar extends Foo {}
class Baz extends Bar {} Emits: var __extends = (this && this.__extends) || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var Foo = (function () {
function Foo() {
}
return Foo;
}());
var Bar = (function (_super) {
__extends(Bar, _super);
function Bar() {
_super.apply(this, arguments);
}
return Bar;
}(Foo));
var Baz = (function (_super) {
__extends(Baz, _super);
function Baz() {
_super.apply(this, arguments);
}
return Baz;
}(Bar)); When I cast a reference from one type to another, it changes at compile-time which methods I am allowed to access on that reference. Although the properties I do access is still verbatim in the source code, the typing constraint has dictated which code is allowed. My point is I don't see the point nor consistency of claiming that TypeScript never emits inserted code from typing information, nor claiming that typing casts don't control which code is allowed. Typeclasses require emitting some code for the declarations and then choosing which are legal at the "cast" of the function use site. I think the distinctions you are making are arbitrary (pigeonholed) in a holistic sense, and thus I don't see what is the utility of your distinctions. In any case, I am not interested in arguing about what the philosophy of TypeScript is. You all have stated you won't do typeclasses, and it is a sufficient answer. I don't like to waste my time arguing about subjective preferences. If anyone cares to clearly explain the utility and objectiveness of TypeScript's design philosophy, I think that might help readers and perhaps even myself. |
That's backporting emit, that doesn't count, you should show us what it does with an ES2015 target instead. |
@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 |
|
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). |
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. |
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. |
@shelby3 I'd like to clarify that the private fields proposal is On Sat, Sep 17, 2016, 22:12 shelby3 notifications@github.com wrote:
|
Note that WeakMap can be fully polyfilled in ES5. I still think that the proposed private fields could be fully down emitted to ES5. |
@kitsonk I was mainly referring to the memory characteristics. (Using a On Fri, Sep 23, 2016, 05:19 Kitson Kelly notifications@github.com wrote:
|
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 |
@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.) |
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? |
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? |
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 typeB
input, typeclasses conceptually enable you to declare the implementation of typeB
for typeA
and invoke the said function with the said instance of typeA
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 typeB
properties on the typeA
instance.The has efficiency, SPOT, and DNRY advantages over the alternative of manually wrapping the instance of
A
in a new instance which hasB
type and delegate to the properties ofA
. Scala hasimplicit
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 functionA
is assigned to the instance'sprotoype
, thus providing the initial implementation for the instance (of typeA
) of the properties of typeA
. If we want to add an implementation of constructor functionB
(i.e. of typeB
) for all instances of typeA
, then we can setA.prototype.prototype = B.prototype
. Obviously we'd like to type check thatA
is already implemented forB
, so we don't attempt to implementB
forA
thus settingB.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) 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.
The text was updated successfully, but these errors were encountered: