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

the typeclass model offers superior extensibility #10844

Closed
shelby3 opened this issue Sep 10, 2016 · 136 comments
Closed

the typeclass model offers superior extensibility #10844

shelby3 opened this issue Sep 10, 2016 · 136 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@shelby3
Copy link

shelby3 commented Sep 10, 2016

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) 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.

@shelby3
Copy link
Author

shelby3 commented Sep 12, 2016

@shelby3 wrote:

And do note that even on the untyped (uni-typed) ECMAScript, the prototype chain is form of heritage of objects since it is global to all constructed instances which didn't override the prototype as constructed (and even retroactively so which is the point I want to explore modeling with typeclasses for potentially amazing benefits in productivity and extensibility).


@shelby3 wrote:

@isiahmeadows wrote:

I'd recommend not trying to force nominal types into TypeScript in a way that requires frequent explicit casting in the meantime, though. If you focus on the data instead of the interface, you'd be surprised how far duck typing actually gets you.

I intend to convince[1] that subclassing is a non-extensible anti-pattern and typeclasses is the superior paradigm for nominal typing.

[1] See the THE COVARIANCE OF GENERICS section and the link back to a TypeScript issue.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Sep 12, 2016
@SimonMeskens
Copy link

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.

@svieira
Copy link

svieira commented 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 like anotherUniverse.$x1z724_q3() // SomeNominalPrototype.someMethod as of yesterday.

@gcnew
Copy link
Contributor

gcnew commented Sep 13, 2016

@SimonMeskens @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:

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 string and number

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')

PS: see https://en.wikipedia.org/wiki/Type_class

@SimonMeskens
Copy link

@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
Copy link
Contributor

gcnew commented Sep 13, 2016

@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
Copy link

@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
Copy link
Author

shelby3 commented 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:

@aluanhaddad wrote:

I would definitely be curious to see your formal type class proposal.
...
Walking the prototype chain and doing reference comparisons is far from reliable.

I think that as you have yourself stated, a different language, one targeting a type aware VM may well be the only way to achieve your goals.

To the extent that TypeScript can embrace unreliability of expected structural (interface and subclassed) types at runtime (even possibly with optional runtime nominal and structural checks that exception to a default or error case), I think perhaps the similar level of typing assurances can be attained with typeclasses. Typeclasses would only make sense nominally, as otherwise they are same as the structural interfaces we have already.

And I believe typeclasses are much more flexible for extension, compared to subclassing. I intend to attempt to explain that soon in the issue thread I created for that.

If we are going to add some nominal capabilities that are compatible with JavaScript's existing paradigms, such as instanceof, then typeclasses would give us the flexibility of extension we get with structural types. Note that instanceof may become much more reliable.

P.S. you are referring to the comment I made about Google's SoundScript and that if they succeed to accomplish runtime soundness, I believe it will essentially be a much different language.


@shelby3 wrote:

@spion wrote:

I was not referring to subclassing. That was only the mechanism via which I illustrated the problem with nominal types.

Afaics, the problem you are concerned with only applies to subclassing.

Typeclasses only solve the initial step where you have to convince the owner to implement your nominal interface; you no longer need to do that - you can write your own instance. However, you still have to convince the first author as well as existing library writers to adopt your typeclass i.e. write their code as follows:

function f(x:TheNominalInterface) { ... }

rather then write their functions against the original nominal type written by the first author:

function f(x: TheNominalOriginalPromise) { ... }

Anyone can implement the typeclass for any preexisting data type. There is no owner any more with typeclasses. The data type can still encapsulate (via modules and/or class) any facets it wishes to, so there is an owner in that respect, but anyone can implement a typeclass interface employing the public interface of any type. It is all about composability and we build new interfaces on top of existing ones. Afaics, the main difference from subclassing is that we are encouraged to invert the hierarchy of inheritance, such that we don't implement a fixed set of interfaces conflated together in one class, but rather implement an unbounded future number of interfaces separately for any class. We change the way we design and think about our interfaces.

And this allows us many benefits, including being able to add an interface to collection of instances and feed it to a function without having to manually rebuild the collection wrapping each instance in a new delegate wrapper shell instance (or in the case of a dynamic runtime, add properties manually to each instance...messing with the prototype chain is more analogous to typeclasses afaics).

@shelby3
Copy link
Author

shelby3 commented Sep 14, 2016

@shelby3 wrote:

@spion wrote:

I don't deny that typeclasses are better and more flexible than subclassing-based interfaces. But the problem doesn't just apply to subclassing.

Here is an instance of the problem in Haskell where despite having typeclasses, the community still haven't been able to fix the fragmentation that resulted from String, ByteString, ByteString.Lazy, Textand Text.Lazy. Libraries simply aren't adopting one common nominal interface: most still just use Text/String/ByteString directly, others implement their own personal and incompatible typeclass, and so on.

With structural types, you can implement the entire interface of an existing type and make all libraries that utilise the exiting type compatible with the new implementation. Without those libraries doing any changes whatsoever.

Per @SimonMeskens's suggestion, please continue this discussion in the issue thread I started for discussing typeclasses. I copied this reply over there. Thanks.

The problem you refer to (as well as Haskell's inability to do first-class unions) is because Haskell has global type inference. So this means that if we implement a data type more than one way on the same typeclass target, then the inference engine can't decide which one to apply. That problem is not with typeclasses, but with Haskell's choice of a globally coherent inference. Haskell's global inference has some benefits, but also has those drawbacks.

To enable multiple implementations (each for a specific typeclass) of the same data type on the same typeclass, we can support declaring typeclasses that extend other typeclasses. TypeScript's existing interfaces can serve this dual role I think. Then at the use site, we can provide a mechanism for selecting which implementation (i.e. sub-interface) is to be used. We can get more into the details in the future.

Suffice it to say that they are just as flexible as structural typing in terms of extensibility. The differences are they provide the ability to group structure nominal at any granularity we choose as an API designer. And to distinguish between equivalent structure that has a different name (nominal type).

@spion
Copy link

spion commented Sep 14, 2016

Then at the use site, we can provide a mechanism for selecting which implementation (i.e. sub-interface) is to be used.

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?

@shelby3
Copy link
Author

shelby3 commented 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 this is 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 prototype chain at will, adding or removing implementations for all existent instances of the constructor function. For a statically compiled language, the compiler can manage this prototype chain 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 global prototype chain 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-instance prototype chain can I think suffice assuming implementations are only added and never removed.

I don't know to what extent this pattern of altering the prototype of properties post-construction has been used in the JavaScript ecosystem?


Another related issue is that type parametrized collections/containers (e.g. List or Array) 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 | string is not compatible with a concrete instance of type of number | 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
Copy link

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
Copy link
Author

shelby3 commented Sep 15, 2016

@svieira wrote:

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 like anotherUniverse.$x1z724_q3()

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.

@shelby3
Copy link
Author

shelby3 commented Sep 15, 2016

@SimonMeskens wrote:

It seems that certain concepts that can only be typed as higher kinds in Haskell

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 this self typing is a feature that emulates one feature HKT could do.

@shelby3
Copy link
Author

shelby3 commented Sep 15, 2016

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 prototype chain of the instance of the class, because there may exist other references to that instance in other asynchronous code paths which will require a different set of typeclass implementations simultaneously, where simultaneous is multiple asynchronous events and their code paths that are interleaved in order of execution.

So the methods of our implementations will not refer to the this on the object dictionary for the implementations and instead will input an function argument for reference to the instance. We can create the object for each typeclass interface implementation separately and then if the use site requires an intersection (aka conjunction) of typeclass interfaces, we can handle this orthogonally. It turns out that we don't use the prototype chain (because it would force us to set the prototype on each implementation object so we couldn't reuse these objects).

So let's write some hypothetical code presuming that we will reuse TypeScript's class for declaring data types and add trait for declaring typeclasses interfaces. To implement a class for a typeclass trait we won't employ an implements clause bound to the class declaration. Instead we'll need a new syntax for declaring the implementation of a pre-existing class.

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 trait names can't contain underscores):

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?

@shelby3 shelby3 changed the title prototype chain could be typed with the typeclass model the typeclass model offers superior extensibility Sep 15, 2016
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Sep 15, 2016

Why do the emitted foo and fooBar functions introduce shadowing C_Foo and C_Bar parameters? Why are you passing a number to bar, which claims to accept string? This code just throws exceptions so it's hard to understand what the exact intent is.

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))

@shelby3
Copy link
Author

shelby3 commented Sep 15, 2016

@RyanCavanaugh wrote:

Why do the emitted foo and fooBar functions introduce shadowing C_Foo and C_Bar parameters?

Because the instances of C_Foo and C_Bar may not be declared in same lexical scope as those use site functions, i.e. the shadowing won't generally exist. This example didn't show that.

Why are you passing a number to bar, which claims to accept string?

Typo. Fixed. Thanks.

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);
}

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 foo and fooBar so the instance can be clean for attaching other interfaces at other use sites in order to enable the extensibility (swapping out implementations) of typeclasses, and as I explained in my prior comments your code is entirely incompatible with asynchronous programming if other use sites and references to your instance of C expect different implementations.

@RyanCavanaugh
Copy link
Member

What I'm saying is, this code unconditionally crashes, so I can't reason about what your intent with it is. The parameter C_Foo is undefined in foo and the code throws an exception because undefined is not a function. What does a non-crashing version of the proposed emit look like?

@shelby3
Copy link
Author

shelby3 commented Sep 15, 2016

@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.

@SimonMeskens
Copy link

SimonMeskens commented Sep 15, 2016

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.

@shelby3
Copy link
Author

shelby3 commented Sep 15, 2016

@SimonMeskens wrote:

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

Re-read my reply to @RyanCavanaugh. I explained why his code is not equivalent.

@SimonMeskens
Copy link

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.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Sep 15, 2016

@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.

@shelby3
Copy link
Author

shelby3 commented Sep 15, 2016

@SimonMeskens wrote:

I said your example adds no new functionality to the language imo, except for a weak version of already proposed extension methods.

Please show me how you can fix the problems I outlined to @RyanCavanaugh with the existing features of TypeScript.

@RyanCavanaugh wrote:

please stop editing your comments to add substantially new information to them

That is an unreasonable request. Sometimes that is not possible to adhere to. Please be tolerant.

It also makes it look like I'm pointing out things which are no longer true

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:

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
Copy link

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?

@spion
Copy link

spion commented Sep 18, 2016

Even casting the class or interface type can change the emitted code in terms of which methods may be called. It is not true that typing in TypeScript doesn't change the code that is emitted or allowed.

Please go ahead and provide an example 😀

@SimonMeskens
Copy link

SimonMeskens commented Sep 18, 2016

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?

@shelby3
Copy link
Author

shelby3 commented Sep 18, 2016

@SimonMeskens wrote:

you're showing the world

Hyperbole.

You're not holding a mirror up

I am also not blind in both eyes.

Any objects that share the same delegation object are of the same type

{ ... } undifferentiated from Object.prototype is uni-typing. As I wrote, "sometimes..."

As for TypeScript not being a superset, how is it not?

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.

@SimonMeskens
Copy link

SimonMeskens commented Sep 18, 2016

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, { ... } is a literal, it's just syntactic sugar. That's like saying that Java doesn't have a string type, because it has string literals and string constructors.

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.

@yortus
Copy link
Contributor

yortus commented Sep 18, 2016

@RyanCavanaugh wrote:

Emitted TypeScript code never changes based on the types of the expressions being transpiled

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 foo and bar are identical apart from a single type annotation, but the different type annotation results in a different emit. In particular the type annotation MyClass has resulted in the MyClass constructor function appearing in the emitted code in a value position.

[EDIT: removed quotes from @spion and @SimonMeskens that refered to something else]

@yortus
Copy link
Contributor

yortus commented Sep 18, 2016

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 Promise<T> type, or be omitted altogether. The inability to use structural typing here does impose some limitations on the type system, such as described here.

@SimonMeskens
Copy link

Yortus, here's the original quote:

Even casting the class or interface type can change the emitted code in terms of which methods may be called

Your example is indeed weird, but not a cast.

@yortus
Copy link
Contributor

yortus commented Sep 18, 2016

@SimonMeskens it is a counterexample of @RyanCavanaugh's quote. EDIT: I'll remove your and @spion's quotes in my comment for accuracy sake.

@SimonMeskens
Copy link

You quoted @spion and I, we both were not talking about @RyanCavanaugh's quote.

@yortus
Copy link
Contributor

yortus commented Sep 18, 2016

@SimonMeskens fixed thanks

@shelby3
Copy link
Author

shelby3 commented Sep 18, 2016

A is superset of B means that any B is valid A, but not every A is valid B.

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 any? No! I only advocated adding features to TS.

I dare you

I need to sleep sometimes. Patience.

As for the type/uni-type discussion, I don't want to get super deep into it, but for one, { ... } is a literal, it's just syntactic sugar. That's like saying that Java doesn't have a string type, because it has string literals and string constructors.

No the point is it doesn't just have a nominal type undifferentiated from Object.prototype unless you consider the properties in the object to not be part of its type. Since I include the name and type of properties declared (but not their values), I say the type is structural and not covered by your original point about a delegation type.

You've not acknowledged my points:

it [TypeScript] offers compile time type safety

Incorrect and incorrect.

The idea is that the compiler promises you that a function is correctly called, not that it can't be incorrectly called.

Nope. It can't promise the invocation is correct at the time it is called.

Of course JavaScript is typed, it just isn't type checked.

It is uni-typed at compile-time. The runtime types may or may not be checked.

The point is that the types can change at run-time and thus there is no typing (only a uni-type of any) which is safe at compile-time.

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.

Replace every instance of the word type with prototype and same discussion.

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.

TypeScript checks contracts of prototypes of objects at compile time

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.

@shelby3
Copy link
Author

shelby3 commented Sep 18, 2016

@spion

Even casting the class or interface type can change the emitted code in terms of which methods may be called. It is not true that typing in TypeScript doesn't change the code that is emitted or allowed.

Please go ahead and provide an example :)

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.

Playground:

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.

@SimonMeskens
Copy link

That's backporting emit, that doesn't count, you should show us what it does with an ES2015 target instead.

@shelby3
Copy link
Author

shelby3 commented Sep 18, 2016

@SimonMeskens wrote:

That's backporting emit, that doesn't count, you should show us what it does with an ES2015 target instead.

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:

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.

And I will repeat some holistic justification might be helpful to some:

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.

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 Symbol tag with instanceof is 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
Copy link
Member

async / await is 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
Copy link
Member

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
Copy link

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
Copy link
Author

shelby3 commented 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
Copy link

@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 notifications@github.com wrote:

@SimonMeskens https://github.com/SimonMeskens wrote:

you're showing the world

Hyperbole.

You're not holding a mirror up

I am also not blind in both eyes.

Any objects that share the same delegation object are of the same type

{ ... } undifferentiated from Object.prototype is uni-typing. As I wrote,
"sometimes..."

As for TypeScript not being a superset, how is it not?

I have a different definition of 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.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#10844 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AERrBMHpTM_QRM5eYy-ahJHQHQzTbiK0ks5qrJ4mgaJpZM4J52xn
.

@kitsonk
Copy link
Contributor

kitsonk commented Sep 23, 2016

without native weak bindings

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
Copy link

@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 notifications@github.com wrote:

without native weak bindings

Note that WeakMap can be fully polyfilled in ES5. I still think that the
proposed private fields could be fully down emitted to ES5.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#10844 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AERrBBA-Qo8degAhkqoVADdw896wcV4dks5qs5mGgaJpZM4J52xn
.

@kitsonk
Copy link
Contributor

kitsonk commented Sep 23, 2016

And the existing userland "polyfill" that is mostly weak is not truly weak IIRC.

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
Copy link

dead-claudia commented Sep 23, 2016

@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
Copy link
Author

shelby3 commented 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
Copy link
Contributor

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?

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests