keean / zenscript

A trait based language that compiles to JavaScript
MIT License
42 stars 7 forks source link

Fast track transpiling to TypeScript? #13

Open shelby3 opened 7 years ago

shelby3 commented 7 years ago

Building off the reasoning and justification in my self-rejected issue Fast track transpiling to PureScript?, I have a new idea of how we might be able to attain some of the main features proposed for ZenScript, without builiding a type checker by transpiling to TypeScript.

If we can for the time being while using this hack, presume that no modules will employ differing implementations of a specific data type for any specific typeclass, i.e. that all implementations of each data type are the same globally for each typeclass implemented (which we can check at run-time and throw an exception otherwise), then the module can at load/import insure that all implementations it employs are set on the prototype chain of all the respective classes' construction functions. In other words, my original point was that JavaScript has global interface injection (a form of monkey patching) via the prototype chain of the construction function, and @svieira pointed out the potential for global naming (implementation) conflicts.

So the rest of the hack I have in mind, is that in the emitted TypeScript we declare typeclasses as interfaces and in each module we declare the implemented data types as classes with all the implemented interfaces in the hierarchy. So these classes then have the proper type where ever they are stated nominally in the module. We compile the modules separately in TypeScript, thus each module can have differing declarations of the same class (because there is no type checking linker), so that every module will type check independently and the global prototype chain is assured to contain the interfaces that the TypeScript type system checks.

So each function argument that has a typeclass bound in our syntax, will have the corresponding interface type in the emitted TypeScript code. Ditto typeclass objects will simply be an interface type.

This appears to be a clever way of hacking through the type system to get the type checking we want along with the ability to have modules add implementations to existing data types with our typeclass syntax. And this hack requires no type checking in ZenScript. We need only a simple transformation for emitting from the AST.

As for the first-class inferred structural unions, TypeScript already has them, so no type checking we need to do.

It can't support my complex solution to the Expression Problem, but that is okay for starting point hack.

I think this is way we can have a working language in a matter of weeks, if we can agree?

That will give us valuable experimentation feedback, while we can work on our own type checker and fully functional compiler.

TypeScript's bivariance unsoundness should be avoided, since ZenScript semantics is to not allow implicit subsumption of typeclass bounds, but this won't be checked so it is possible that bivariance unsoundness could creep in if we allow typeclasses to extend other typeclasses. Seems those bivariance cases don't impact my hack negatively though:

When comparing the types of function parameters, assignment succeeds if either the source parameter is assignable to the target parameter, or vice versa.

Not a problem because of course an interface argument type can never to be assigned to any subclass.

When comparing functions for compatibility, optional and required parameters are interchangeable. Extra optional parameters of the source type are not an error, and optional parameters of the target type without corresponding parameters in the target type are not an error.

When a function has a rest parameter, it is treated as if it were an infinite series of optional parameters.

We should be able to design around this.

Also a compile flag can turn off TypeScript's unsound treatment of the any type.

TypeScript is structural but there is a hack to force it to emulate nominal typing. We could consider instead transpiling to N4JS which has nominal typing and soundness, but it is not as mature in other areas.

keean commented 7 years ago

I had actually planned to start with no optimisation or type checking :-) Simply parse the source, discard the typing information and output the JavaScript. You can then already start experimenting with the syntax and writing simple programs, and you will simply get runtime errors (or bugs) in the JavaScript.

Type-classes are not that simple however, because they don't obviously associate with any given argument to the function. Consider a Cast type class that converts a value of type A to type B, and a function that uses it:

fn f<A, B>(x : A, y : B) where Cast<A, B> =>
    let x_as_B : B = cast(x)
    ...

(aside: we need to decide on where the type-class requirements list is going to go in function definitons).

Cast is does not provide methods on A or B but provides independent functions (all type classes do).

What we actually want to do is turn the type-class into a Record data type, turn the implementation into a value of the type of that Record, and then add an extra parameter to the function. The JavaScript would be something like:

var cast_A_B = {
    cast : function(x) { return x; } // whatever casting function
 }

function f(x, y, cast_class) {
    var x_as_B = cast_class.cast(x)
    ...
}

Note there is no representation of the type-class itself in the JavaScript, it just has the implementations.

In the future we can monomorphise and inline the typeclass function and remove the extra argument where appropriate, but that can be viewed as an optimisation.

shelby3 commented 7 years ago

@keean wrote:

Simply parse the source, discard the typing information and output the JavaScript.

If that was feasible, I would have created ZenScript a long time ago. Your plan is not feasible.

The compiler has to select the dictionary for the implementation of the data type which is passed in the source code, but there is no way for ZenScript to do that without also doing full type checking.

That is why I devised this hack described in the OP.

My hack transforms typeclassing into subclassing within the module, so that a subclassing type checker can check everything. And it requires no typechecking in ZenScript. And the only downside I see, is that it requires that modules don't provide conflicting implementations (which is damn easy for us to do now at this experimentive stage).

So again I ask you do you want to accept my proposal that we first prioritize transpiling to TypeScript? I am probably going to do if you won't. Afaics, there is no other fast track way.

Type-classes are not that simple however, because they don't obviously associate with any given argument to the function.

fn f<A, B>(x : A, y : B) where Cast<A, B> =>

Obviously we can't do that in my hack, but we can do higher-kinds if our transpile target supports them:

fn f<A, B>(x : A<B>) where Cast<A<B>> =>

P.S. would you please stop writing this:

fn f<A, B>(x : A, y : B)

Since (for the 5th time I will state that) I presume we've decided (since I've seen no objections) on the following.

let f(x : A, y : B)

Or:

let f = (x : A, y : B)

We unified around let rather than have an unnecessary fn along with let. And we made type parameters ALLCAPS.

shelby3 commented 7 years ago

@shelby wrote:

  1. Sum, Recursive, Record, and Product parametrized data type with optional Record member names and optional Record (e.g. Cons) and Product (e.g. MetaData) names:

    data List<T> = MetaData(Nil | Cons{head: T, tail: List<T>}, {meta: Meta<T>})

Thinking about how to emit that in a language with classes and tuples. We also need to get the declaration of covariant or contravariant for type parameters correct. Also were is mutability declared above?

sealed interface NilOrCons<T>   // or `abstract class`, note sealed may not be available but that is ok
object Nil implements NilOrCons<Bottom> // or `class` and instantiate a singleton instance
sealed class Cons<T> implements NilOrCons<T> {
  var head, tail
  constructor(head: T, tail: NilOrCons<T>) {
    this.head = head
    this.tail = tail
  }
}
sealed interface List<T>
sealed interface Meta<T>
sealed interface Recordmeta<T> {
  var meta
  constructor(meta: Meta<T>) {
    this.meta = meta
  }
}
sealed class MetaData<T> extends Tuple2<NilOrCons<T>, Recordmeta<T>> implements List<T>

Makes one realize the unified data syntax is much more succinct! :sunglasses:

keean commented 7 years ago

Nil should have the same type as Cons. The reason is we do not know how long the list is at runtime, so how do we type check:

let f<A>(list : List<A>) requires Show<A> =>
    while list /= Nil :
        show(list.head)
        list = list.tail
keean commented 7 years ago

Please no all caps, it's like shouting in my text editor :-(

ML prefixes type variables with a character (happens to be ' but could be anything)

Also I note you want to get rid of the type parameter list, you do realise sometimes you need to explicitly give the parameters if they cannot be inferred like:

let x = f<Int>(some_list)

This also makes me realise another ambiguity in our syntax, it is hard to tell the difference between assigning the result of a function and assigning the function itself as a value. In the above you might have to pass to the end of the line to see if there is a '=>' at the end. This requires unlimited backtracking which you said you wanted to avoid.

shelby3 commented 7 years ago

@keean's prior comment has a response. Please where reasonable to do so, let's try to keep the syntax discussions in their Issue thread. I started the tangent in this case. :hushed:

shelby3 commented 7 years ago

@keean wrote:

Nil should have the same type as Cons. The reason is we do not know how long the list is at runtime, so how do we type check:

let f(list : List<A>) requires Show<A> =>
    while (list != Nil)
        show(list.head)
        list = list.tail

Disagree. And nearly certain you are incorrect.

Assuming:

data List<T> = Nil | Cons{head: T, tail: List<T>}

The list != Nil is a guard that insures list is a Cons in the guarded code block, because otherwise list.head and list.tail must be a compiler error on a polymorphic type List<A>. This is yet another example where you are misapplying your Haskell mindset to a subtyping language. TypeScript already has these guards in their type checker.

But the guard doesn't change the type of list (can't change the type of a reference after it is constructed), it only specializes it where it is necessary. Thus the type of list remains List<A>.

I don't think you should attempt the type checker as our first step. We should gain experience with TypeScript's type checker first, before attempting our own. After that, everything will be clearer to both of us, so we don't commit major blunders on the type checker and lose precious time.

keean commented 7 years ago

So what you are saying is that List<Bottom> <: List<A> where A is any type other than bottom. However Nil cannot have a different type without dependent types as we do not know where the end of the list is until runtime.

shelby3 commented 7 years ago

@keean wrote:

So what you are saying is that List<Bottom> <: List<A>

Yes if by <: you mean subtypeof.

where A is any type other than bottom.

Disagree. List<Bottom> is a subtypeof of itself. Bottom being a subtypeof itself doesn't mean nor necessitate that it can be instantiated (quantified).

However Nil cannot have a different type without dependent types as we do not know where the end of the list is until runtime.

Different type from what? And what is the problem you see? I don't see any problem.

The list != Nil is a run-time test. The types match because Nil is subsumable to a List<A> and the != is defined for List<A> (actually List<_> unless we want deep comparison of all elements) not List<Bottom>. You forgot the trait bound for the != operation, although maybe we can make that implicit requires?

 let f(list : List<A>) requires Show<A>, Eq<List<_>> =>
     while (list != Nil)
         show(list.head)
         list = list.tail

Where _ means not used.

Generally though, I am thinking we will need higher-kinds.

keean commented 7 years ago

In the case of the above datatype Eq<List<A>> would be the correct trait bound.

In the above Nil would not be typed List<Bottom> as there is enough typing information to determine that Nil has the type List<A>, we can tell that because the arguments of /= must have the same type.

In other words we assign Nil the type List<B = Bottom> where B is a fresh unused type variable, then we know list has the type List<A> where A is the type in the function type. We then know the types of /= must be the same so we have to promote Nil to type List<B = A>

shelby3 commented 7 years ago

What the heck is /= in this context? I thought it was a typo and changed it to !=.

keean commented 7 years ago

Do you prefer !=, I have programmed in lots of languages so I know a few, !=, /=, =/=, <> etc...

shelby3 commented 7 years ago

For me and everybody I know who uses a mainstream programming language, /= means divide-equals.

We are targeting JavaScript. The / means divide.

shelby3 commented 7 years ago

@keean wrote:

In the case of the above datatype Eq<List> would be the correct trait bound.

In the above Nil would not be typed List<Bottom> as there is enough typing information to determine that Nil has the type List<A>, we can tell that because the arguments of /= must have the same type.

In other words we assign Nil the type List<B = Bottom> where B is a fresh unused type variable, then we know list has the type List<A> where A is the type in the function type. We then know the types of /= must be the same so we have to promote Nil to type List<B = A>

This is hopeless. You can stop thinking in terms of Haskell's lack of subtyping and subsumption.

Mythical Man Month effect is taking over.

Nil remains List<Bottom>. Its type can't change. It is subsumed as necessary to type check separately in each expression where it appears.

keean commented 7 years ago

Ada uses /= :-) It comes from the Pascal family of languages,

keean commented 7 years ago

@shelby3 wrote:

This is hopeless. You can stop thinking in terms of Haskell's lack of subtyping and subsumption.

I think we should avoid subtyping and subsumption, except specifically for union types where we have set-based subsumption like Int | String :> Int

Edit: Remember we agree on how bottom and top should behave, so its probably just a communication problem :-)

shelby3 commented 7 years ago

@keean I need to go jogging and buy prepaid load for my mobile phone. I suggest we focus on the syntax for now and that you experiment with output to TypeScript (or Ceylon) to get a feel for how these type systems work with subtyping and as we do this transform of our simplest typeclasses.

It is too much verbiage to discuss the typing now.

We can't target Scala as it doesn't have the unions.

keean commented 7 years ago

There's a reason I dont like typescript or Cylon :-)

Anyway above I was thinking about bidirectional type inference, which was a mistake, as I actually want things to be compositional, which would align better with what you want.

My point about not losing track of type variables, and needing more than one bottom in something like Pair<A, B> still stands though.

shelby3 commented 7 years ago

Finished jog. Very fast. Headed to buy load.

Please let us put typing strategies aside for now and focus on the syntax and transpiling to TypeScript. This is what I am going to do. I need a working language yesterday. I don't have time to wait for the perfect language. Sorry.

Let's focus on what we can do quickly.

The perfect language will come incrementally.

keean commented 7 years ago

Then we are stuck with typescripts unsound type system?

shelby3 commented 7 years ago

@keean wrote:

Then we are stuck with typescripts unsound type system?

I documented upthread why I thought we could avoid the aspects that are unsound. If not, we could try N4JS which the authors claim is sound.

The transformation to subclassing will lose some of the degrees-of-freedom that we get with for example multi-type parameter typeclasses, but we can start with single-parameter so at least I can start experimenting with the syntax and typeclass concept.

You may not agree with this direction because you may think it is not correct to model with a subtyping type checker. I think we will end up with a subtyping type checker eventually any way and that you will eventually realize this. If I am wrong, then I am wrong. You think you know, but I doubt either us can know for sure yet.

But what are your options? You can't transpile to PureScript because it doesn't allow impurity. So your only option is to build the entire type checker. But that will take months to get to a working stable, usable state. And there is a non-zero risk that you would realize along the way that your design was incorrect and full of corner cases issues. Then you'd start over again.

So what alternative do you propose?

We both agree that we need an imperative, typeclass language. It seems we mostly agree on the syntax. Our major conflict if any is on implementation and which strategy to prioritize.

shelby3 commented 7 years ago

Then we are stuck with typescripts unsound type system?

http://www.brandonbloom.name/blog/2014/01/08/unsound-and-incomplete/

shelby3 commented 7 years ago

Thinking about if we really need higher-kinds, because TypeScript apparently doesn't have the feature and Ceylon's feature may or may not be adequate. Both of them offer first-class unions, but TypeScript's guards may require less boilerplate (Edit: Ceylon has flow-typing aka typeof guards), we access prototype in native code, and presumably with high certainty TypeScript has more bijective JS emission since it is a minimal superset. Scala offers higher-kinds (and simulation of traits with implicits), also compilation to Scala.JS, but no first-class unions.

A Monoid is an example of an interface that needs both the type of the datatype implemented and the element type of the said datatype.

interface Monoid<A, SELF extends Monoid<A, SELF>> {
    identity(): SELF
    append(x: A): SELF
}

abstract class List<A> implements Monoid<A, List<A>> {
    identity(): List<A> { return Nil }
    append(x:A): List<A> { return new Cons(x, this) }
}
class _Nil extends List<never> {}
const Nil = new _Nil
class Cons<A> extends List<A> {
    head: A
    tail: List<A> 
    constructor(head: A, tail: List<A>) {
        super()
        this.head = head
        this.tail = tail
    } 
}

I thought the above code is higher-kinded, but I guess not because no where did I write SELF<A> and thus compiler isn't enforcing that A is a type parameter of SELF's type constructor. And it compiles and assumes that an implementation of Monoid for Nil and Cons<A> will always be same as the implementation for List<A>, which is why I added run-time exceptions to assert the invariants the compiler can't check. It even compiles if elide the types we couldn't know in order to transpile without any type checking.

Whereas, the following attempt didn't because it doesn't make that assumption.

interface Monoid<A> {
    identity(): this
    append(x: A): this
}

abstract class List<A> implements Monoid<A> {
    identity(): this { return <this>Nil }
    append(x:A): this { return <this>(new Cons(x, this)) } // Error: Type 'Cons<A>' cannot be converted to type 'this'.\nthis: this
}
class _Nil extends List<never> {}
const Nil = new _Nil
class Cons<A> extends List<A> {
    head: A
    tail: List<A> 
    constructor(head: A, tail: List<A>) {
        super()
        this.head = head
        this.tail = tail
    } 
}

And the following attempt didn't compile also because it didn't make that assumption.

interface Monoid<A> {
    identity(): this
    append(x: A): this
}

abstract class List<A> implements Monoid<A> { // Error: Class 'List<A>' incorrectly implements interface 'Monoid<A>'.\n  Types of property 'identity' are incompatible.\n    Type '() => List<A>' is not assignable to type '() => this'.\n      Type 'List<A>' is not assignable to type 'this'.
class List<A>
    identity(): List<A> { return Nil }
    append(x:A): List<A> { return new Cons(x, this) }
}
class _Nil extends List<never> {}
const Nil = new _Nil
class Cons<A> extends List<A> {
    head: A
    tail: List<A> 
    constructor(head: A, tail: List<A>) {
        super()
        this.head = head
        this.tail = tail
    } 
}
keean commented 7 years ago

Well both of those are having to bend a monoid to fit their objects only systems (see my post on objects or no objects).

A monoid is simply this:

pluggable Monoid<A> :
    mempty : A
    mappend(A, A) : A

Note in a non object system it has no self reference and no need for higher kinds, they are both free functions like string literals and string concatenation. So using that example:

String implements Monoid :
    mempty = ""
    mappend = +

And we can use it like this:

let x = mappend(mempty, mempty)

Note mappend and mempty are just functions not methods, just like "" and + are not methods. Infact my proposal for "no objects" would mean no methods at all, you simply create a data-type and implement the type-classes you want on it. Namespacing is handled by modules.

Regarding why your implementation did not work, try:

interface Monoid {
    identity(): this
    append(x: this): this
shelby3 commented 7 years ago

Regarding why your implementation did not work, try:

interface Monoid {
    identity(): this
    append(x: this): this

I tried but it won't type check and Monoid has incompatible semantics with a list.

Readers see also the related discussion in the Higher-kinds thread.

shelby3 commented 7 years ago

@keean wrote:

A monoid is simply this:

pluggable Monoid<A> :
    mempty : A
    mappend(A, A) : A

That type checks in TypeScript, but again Monoid has incompatible semantics with a List.

Whether this is an explicit or implicit function argument seems only to be an incidental concern. The problem we want to avoid from OOP by preferring typeclasses is the premature binding of interface to inheritance hierarchy, aka subclassing.

shelby3 commented 7 years ago

ML functors are similar to typeclass implementations, e.g. implementing ITERABLE for a FOLD interface. And in OCaml functors are first-class, and now even have implicit selection by the compiler in the static scope. And BuckleScript appears to be a good OCaml -> JavaScript compiler.

So just as is the case for Scala, we could I think entirely emulate our typeclasses in OCaml, but ML variants do not have first-class anonymous structural unions inference, which TypeScript, Ceylon, and N4JS have (yet they don't have typeclasses). So we'd end up with a lot of boilerplate on our language's source code (not just in the emitted transpiled code), which is antithetical to the elegance and brevity of the code I want.

I think I would rather have elegance and brevity and be limited on the typeclasses we can write by transpiling to TypeScript, then starting off without the first-class anonymous structural unions inference and have noisy code that is perhaps more generally capable in the area of typeclasses. Also I don't want to waste a lot of effort making it perfectly interact with OCaml. The goal is to get something quick, elegant, and dirty by transpiling to TypeScript to test out the "look & feel"; and then work on our own type checker which can do all the features we want, not just what shoehorning into OCaml will allow us to do. Also there is factor that I have no experience with ML variants and the syntax currently looks foreign to me.

keean commented 7 years ago

Here is a solution that works. I am not sure it is the best solution:

class Monoid<A> {
    identity(): A
    append(x:A, y:A): A
    constructor(
        identity: () => A, append: (x: A, y: A) => A) {
        this.identity = identity;
        this.append = append;
    }
}

abstract class List<A> {}
class _Nil extends List<never> {}
const Nil = new _Nil
class Cons<A> extends List<A> {
    head: A
    tail: List<A> 
    constructor(head: A, tail: List<A>) {
        super()
        this.head = head
        this.tail = tail
    } 
}

let int_list_monoid = new Monoid<List<Int>>(
    (): List<Int> => new Cons(head = 'I', tail = Nil),
    (x: List<Int>, y: List<Int>): List<Int> => if x == Nil {return y; } else {
        return new Cons(x.head, int_list_monoid.append(x.tail, y)))
);

let test = int_list_monoid.append(int_list_monoid.identity(), int_list_monoid.identity())
console.log(test);
keean commented 7 years ago

There is something weird though because the list is type List<Int> but typescript lets me put strings in.

shelby3 commented 7 years ago

@keean wrote:

Here is a solution that works. I am not sure it is the best solution:

Why are you repeating the solution I provided in a prior comment?

keean commented 7 years ago

refresh :-) it shares the original not the edited version from the playground for some reason. I have pasted the code in directly now.

shelby3 commented 7 years ago

@keean wrote:

it shares the original not the edited version from the playground for some reason.

To work around that Playground bug, first delete everything from the url including and following the # on the Address line of the browser. Reload new url (type Enter, don't click Refresh). Then click Share. Appears to be a cookies issue. :cookie: :japanese_ogre:

keean commented 7 years ago

Try This

shelby3 commented 7 years ago

Okay I fixed yours, and you are correct that List can implement Monoid semantics by completely rebuilding the List.

In my prior comment on this, I wasn't paying attention that Monoid does not require mutating its input, i.e. the result may (must?) be a different instance than the input. Which is probably required by its laws, but I didn't check those.

But the challenge is to make a syntax for our programming language to express that, such that we can transpile to that without a type checker?

keean commented 7 years ago

Okay I agree with all your changes except I don't see any advantage of splitting the Monoid class into an interface and a class.

Also typescript does not seem to catch the type error of identity Cons-ing a String "I" into what is supposed to be a numeric list? Is type-checking even turned on in that playground?

shelby3 commented 7 years ago

Note if use different name for class and interface Monoid, then the compiler forces to declare the members identity and append in the class also. So this is why if I remove the initialization in the constructor, there is no error, because those members are defaulted to null. TypeScript I believe has a compiler flag to remove the default population of every type with null.

shelby3 commented 7 years ago

@keean wrote:

Okay I agree with all your changes except I don't see any advantage of splitting the Monoid class into an interface and a class.

So that when we emit typeclass bounds on function arguments, we don't allow the function to invoke the constructor of _Monoid.

Also typescript does not seem to catch the type error of identity Cons-ing a String "I" into what is supposed to be a numeric list? Is type-checking even turned on in that playground?

This change to our example explains that it is the "bivariance" unsoundness of TypeScript, i.e. that when it has to subsume to List<A> it does not enforce covariant relationship of the type parameter A. We could experiment with N4JS if we prefer enforced soundness. Or both?

OTOH, TypeScript being so "forgiving" (completeness instead of soundness) may make it easier for us to experiment bypassing its type system. Perhaps could employ compiler generated run-time checks where we expect type system holes.

shelby3 commented 7 years ago

@shelby3 wrote:

But the challenge is to make a syntax for our programming language to express that, such that we can transpile to that without a type checker?

If we support pattern matching overloads for functions:

typeclass Monoid<T>
   identity: T
   append(T): T
List<A> implements Monoid
   identity = Nil
   append(y) =>
      let f(Cons(head, tail), y) => Cons(head, f(tail, y)),
          f(Nil,              y) => y
      f(this, y)

Otherwise:

typeclass Monoid<T>
   identity: T
   append(T): T
List<A> implements Monoid
   identity = Nil
   append(y) =>
      let f(x, y) => match(x)
         Cons(head, tail) => Cons(head, f(tail, y))
         Nil              => y
      f(this, y)

The second variant above seems transpilable to this TypeScript code without any type checking. If we want to get rid of that if (x instanceof _Nil) then our type checker has to know that Nil is the only other alternative for List<A>. I suppose the first variant example above only requires a syntactical transformation to the second.

Can we somehow get ES6's tail recursion to help avoid stack overflows and gain performance in the above example?

shelby3 commented 7 years ago

We can shorten that:

typeclass Monoid<T>
   identity: T
   append(T): T
List<A> implements Monoid
   identity = Nil
   append(y) => match(this)
      Cons(head, tail) => Cons(head, tail.append(y))
      Nil              => y

And the corresponding TypeScript code.

keean commented 7 years ago

The problem is 'identity' should be a constructor function, not a method of list, so on the same basis append needs two arguments like the + operator.

If you use 'this' the type of monoid needs to be:

typeclass Monoid
   static identity: This
   append(This): This

But then identity has to have a static type.

typeclass Monoid<T>
   identity: T
   append(T, T): T

Now identity has the correct type, but append has two arguments, both are now free functions not methods.

shelby3 commented 7 years ago

@keean wrote:

The problem is identity should be a constructor function, not a method of list

Agreed:

typeclass Monoid<T>
   static identity: T
   append(T): T
List<A> implements Monoid
   identity = Nil
   append(y) => match(this)
      Cons(head, tail) => Cons(head, tail.append(y))
      Nil              => y

We can implement it on TypeScript as a method, because our transpiler can easily check to make sure this is not accessed within implementations of static.

Btw, in case you might think I didn't know this, I had the STATIC in my mockup for my language Copute last modified in 2013. You can see I was working on this in 2012.

keean commented 7 years ago

Great, I will read that. The problem with identity is how do you call it without an object to call it on? If you look at my coding in typescript, I create a class for the Monoid dictionary, and then an object of that class. That object is the type-class dictionary for the List type. We use that object to hold the methods, so append needs two arguments because the object it is part of is neither of the lists being appended. Personally I find this makes things more complex because of its recursive type. I think things are simpler and easier without the this self reference.

However does getting rid of this take us too far from the mainstream? Can we sell this as a C like scripting language plus type-classes?

Edit: Java and 'C' seem equally popular so having objects is definitely not a hard requirement. However there are more languages that feature objects like Python etc.

shelby3 commented 7 years ago

@keean wrote:

Great, I will read that.

I understand better now of course so there were errors. But some of that laid the foundation for my understanding.

The problem with identity is how do you call it without an object to call it on?

If the use-site requests a Monoid typeclass interface in the where clause and it not the bound of an input argument, then pass an extra argument which is the dictionary for the static methods of Monoid. See static_List in my TypeScript code example.

If you look at my coding in typescript, I create a class for the Monoid dictionary, and then an object of that class. That object is the type-class dictionary for the List type. We use that object to hold the methods, so append needs two arguments because the object it is part of is neither of the lists being appended.

Yours loses genericity on A at the implementation side (so we would need a type checker to track every instantiation of List<A> and know which of your constructed instances to pass along with the instance at the use-site!), because you would need higher-kinds to code it generically. Scala can do that with higher-kinds:

trait Monad[+M[_]] {
  def unit[A](a: A): M[A]
  def bind[A, B](m: M[A])(f: A => M[B]): M[B]
}

My design is so we don't need a type checker to transpile it to an OOP language without typeclasses.


@keean wrote:

Personally I find this makes things more complex because of its recursive type. I think things are simpler and easier without the this self reference.

However does getting rid of this take us too far from the mainstream? Can we sell this as a C like scripting language plus type-classes?

From the naive programmer's perspective, they are passing an instance argument to the call site, and they expect the typeclass interface (bound) to operate on that instance, so they are probably thinking in terms of this for the instance and their familiarity with OOP. Only the advanced users of the language will learn to request static typeclass interfaces in the where clause (or will it be a requires clause?). I think the explicit annotation with static is more clear to those coming from OOP. Can you give an example of any problem this causes?

P.S. I am very sleepy (delirious), so check the above for errors. :zzz: :sleepy:

shelby3 commented 7 years ago

Follow-up on prior comment.

shelby3 commented 7 years ago

@shelby3 wrote:

I don't think the most frequent case will be implementing typeclasses by pairs of data types. I think single data type implementations will be much more common, and Eq and Add will not work with pairs of types, but rather the same type for both arguments. We want to avoid implicit conversions, as that appears to be a design error in many languages such as C and JavaScript. Please correct me if you know or think otherwise.

As I had argued, it doesn't seem to make any sense to a programmer coming from OOP, that they've specified an interface bound on their function argument, and they are not suppose to call the methods using dot syntax. I think it is breaking the way mainstream (Java, C++, JavaScript, PHP, etc) people naturally think about it. I explained that static makes it explicit when this doesn't apply. I much prefer explicitness in this case, as it is not any more verbose.

I prefer to keep out typeclass concept as familiar as possible to the way people already think, especially given there are no downsides to doing so.

Additionally, by using dot notation on calls, afaik we improve parsing performance in the LL(inf) case.

Afaics, we don't always need requires in the single data type implementation case (i.e. one type parameter on the typeclasse), and can write it like this:

f(x: IShow) => x.show()

Instead of:

f(x: A) requires IShow<A> => x.show()

For two or more typeclass interfaces on the same input:

f(x: ILog+IShow) =>
   x.log()
   x.show()

For IEq though we need requires because x and y need to be the same data type:

f(x: A, y: A) requires IEq<A> => x.equals(y)
f(x: A, y: A) requires IEq<A> => y.equals(x)
f(x: A, y: A) requires IEq<A> => x === y

I presume the implicit conversions case, if we support it would be:

f(x: A, y: B) requires ICongruent<A, B> => x == y

Btw, when a zero argument method returns a value and we are not discarding the result, do we want to allow as Scala does?

f(x: ILength) => x.length

Instead of:

f(x: ILength) => x.length()

P.S. Note the I prefix for typeclasses is a proposal, but I don't know if we accepted that or not. On the one hand, it makes it clear which are typeclasses and which are data types, but OTOH it is noisy.

keean commented 7 years ago

I think leaving off the () on zero argument functions is not a good idea, because those functions execute. How do you pass a zero argument function as an argument to another function if you do not indicate when it is to be executed. You effectively lose control over the order in which side-effects happen.

shelby3 commented 7 years ago

@keean wrote:

How do you pass a zero argument function as an argument to another function if you do not indicate when it is to be executed

Based on the type it is being assigned to, although there is a corner case where the result type is the same as the type being assigned to.

I think Scala allows the length(_) in that corner case. We could require this always for passing the function instead of its result value.

On the positive side, it does help to get rid of parenthesis more times than it will cause confusion.

keean commented 7 years ago

I think Scala allows the length(_)

keean commented 7 years ago

I think Scala allows the length(_)

That's worse :-( I think function application.needs to be consistent not some backwards special case, we seem to be introducing corner cases into the grammar. Scala is not a good model to follow.

shelby3 commented 7 years ago

@keean wrote:

I think Scala allows the length(_)

That's worse :-( I think function application.needs to be consistent not some backwards special case, we seem to be introducing corner cases into the grammar. Scala is not a good model to follow.

Perhaps you misunderstood. I had explained to you that the way we can do partial application is using the _ for the arguments we don't want to pass.

So for zero argument functions, if we want to allow the shortform of calling them length instead of length(), then we also need some way of differentiating when we don't want to call them but want the function instance instead. So the solution is to maintain our consistent rule of partial application and specify that the non-existent first parameter is not passed length(_).