Open shelby3 opened 8 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.
@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.
@shelby wrote:
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:
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
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.
@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:
@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.
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.
@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.
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>
What the heck is /=
in this context? I thought it was a typo and changed it to !=
.
Do you prefer !=
, I have programmed in lots of languages so I know a few, !=
, /=
, =/=
, <>
etc...
For me and everybody I know who uses a mainstream programming language, /=
means divide-equals.
We are targeting JavaScript. The /
means divide.
@keean wrote:
In the case of the above datatype Eq<List> would be the correct trait bound.
In the above
Nil
would not be typedList<Bottom>
as there is enough typing information to determine thatNil
has the typeList<A>
, we can tell that because the arguments of/=
must have the same type.In other words we assign
Nil
the typeList<B = Bottom>
whereB
is a fresh unused type variable, then we know list has the typeList<A>
whereA
is the type in the function type. We then know the types of/=
must be the same so we have to promoteNil
to typeList<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.
Ada uses /=
:-) It comes from the Pascal family of languages,
@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 :-)
@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.
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.
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.
Then we are stuck with typescripts unsound type system?
@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.
Then we are stuck with typescripts unsound type system?
http://www.brandonbloom.name/blog/2014/01/08/unsound-and-incomplete/
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
}
}
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
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.
@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.
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.
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);
There is something weird though because the list is type List<Int>
but typescript lets me put strings in.
@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?
refresh :-) it shares the original not the edited version from the playground for some reason. I have pasted the code in directly now.
@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:
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?
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?
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.
@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 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?
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.
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.
@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.
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 Listthis
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.
@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 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
andAdd
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 whenthis
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.
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.
@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.
I think Scala allows the length(_)
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.
@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(_)
.
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 theprototype
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 theprototype
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
interface
s and in each module we declare the implemented data types asclass
es with all the implementedinterface
s 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 sameclass
(because there is no type checking linker), so that every module will type check independently and the global prototype chain is assured to contain theinterface
s 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 aninterface
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:
Not a problem because of course an
interface
argument type can never to be assigned to any subclass.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.