chapel-lang / chapel

a Productive Parallel Programming Language
https://chapel-lang.org
Other
1.78k stars 418 forks source link

Interfaces #18921

Closed skaller closed 2 months ago

skaller commented 2 years ago

Interfaces questions and comments..

skaller commented 2 years ago

Oops. Sorry about above mess. I've read the docs but some questions remain and some issues. Not sure when to split of an actual issue. Lets start here:

The general syntax (without sugar) for interfaces must start like this:

interface X (T1:K1, T2:K2) where C1(T1, T2), C2(T2,T2) { ...}
  1. An interface must allow more than one type parameter. A simple example would be Drawable(Shape, Surface). Standard Haskell didn't support this but it is a serious a design blunder as object orientation. In the CHIP2 paper (I note Siek is one of the authors!), it isn't clear to me if this is supported but it is imperative this be in the first implementation, otherwise assumptions will be made in the semantics, syntax, and implementation which will be hard to undo later when there is a need to generalise. It's much better to be fully general first. It also helps clarify the issues by eliminating ones that come up when only one type is permitted.
  2. The K1 and K2 are kind specifications. A kind is a meta type. Chapel will need a kinding system. One of the key reasons to introduces distinct kinds comes from implementing substructural logic. Chapel already has shared, owned, borrowed and unmanaged kinding attributes for classes. Now I will show an error:
    proc diag(x:?T:shared) : tuple(?T,?T) { return (x,x); }
    var own = ... // an owned class
    .. diag(own,own) ..

    The error here is that own has kind owned and so may not be passed to a polymorphic function for which the type has shared kind. Lets recode it:

    proc diag(x:?T:owned)  : tuple(?T,?T) { return (x,x); }

    Now the call is correct, but the diag function itself is in error because it duplicates an owned value which is not permitted. Without the kinding constraint on the type variable, there would be no way to detect this error. Haskell kinding system is bugged. It has only one primitive kind * which means shared plus combinators. Felix has multiple kinds which actually obey subkinding relations as well.

Kinding constrains applied to type variables are directly analogous to type constraints applied to value variables. They're not required if the only kind is shared. In Felix, this is the default. Owned values are moved, not copied. Rust uses type classes here not kinds. However it starts with uniqueness types and then exempts copyable types with a type class I think. It MAY be possible to use type classes to handle this. This is an open issue, at least for me.

However, in the event it is required the sugar proposed for constraints:

interface X(T1:  Comparable) ...

must be rejected because it would not be possible to distinguish a kinding constraint from a type class constraint.

Now, there is another reason why kinds are essential in type class interfaces, and I can only show and example from Felix to demonstrate:

class Monad [M: TYPE->TYPE] {
  virtual fun ret[a]: a -> M a;
  virtual fun bind[a,b]: M a * (a -> M b) -> M b;
  fun join[a] (n: M (M a)): M a => bind (n , (fun (x:M a):M a=>x));
}

I don't know exactly how this would look in Chapel. This is real working code. This type class has a single parameter, but it is NOT a type! It is actually a type function. The kind shows that: it is a mapping from TYPE to TYPE. Whether or not Chapel ever implements this, you do not go around precluding extensions by using bad syntax. A general principle of language design is that every symbol must, at the point of definition, be allowed to have an explicit descriptive annotation, for example an ordinary variable must permit a type specification (even if it is not necessary), and a type variable, similarly, must permit a kinding specifcation.

I will note, I have made this mistake in Felix, not in type classes, but in functions, where I allowed the kind of sugar suggested for Chapel, and it was fine because I could tell the difference between a kind and an interface constraint, because, there were only a few fixed kinds. But now .. I am introducing kind variables and now I cannot tell any more.

skaller commented 2 years ago

BTW as a general guide I recommend considering introducing interfaces without any sugar at all. This will force users to write everything out long hand, including Chapel standard library developers. This means the users will truly understand what they're doing (or not!) and limit confusion about what sugar does what to zero (because there isn't any to be confused about), given the actual design is unfamiliar. That way any issue can be resolved before introducing the sugar.

Generally when you do introduce sugar it is good if it can be implemented directly in the parser. This means the bulk of the compiler doesn't need any changes. If you cannot do this, the sugar may be suspect.

Parser sugar can be very powerful. A key example from Felix, implemented in the parser, is that f x and x.f mean the same thing. In other words, for any function application in forward polish form, you can always equivalently write it in the reverse polish form. This turns out to have profound implications for coding. The guarantee is universal. No ifs, buts, exceptions, or special cases are possible because the parser does the work. Which in turn means the programmer can rely on it completely.

If your sugar cannot be implemented in the parser, you need to pause and ask, "why not?".

skaller commented 2 years ago

Another issue: marking instantiable functions in interfaces. Here's a Felix example:

class FloatMultSemi1[t] {
  inherit Eq[t];
  proc muleq(px:&t, y:t) { *= (px,y); }
  fun mul(x:t, y:t) => x * y;
  fun sqr(x:t) => x * x;
  fun cube(x:t) => x * x * x;
  virtual fun one: unit -> t;
  virtual fun * : t * t -> t;
  virtual proc *= (px:&t, y:t) { px <- *px * y; }
  reduce id (x:t): x*one() => x;
  reduce id (x:t): one()*x => x;
}

The virtual keyword means the function is instantiable. The other functions are defined in terms of the virtuals. This is similar to classes in say C++, where some method can be overridden and some not. The ones that cannot be instantiated are still available when a function requires the type class (if they're public). You view the virtuals as axioms and the non-virtuals as lemmas.

Notice also, a virtual can be defined! This is a default implementation. In the example, a mutation is defined as an assignment of a functional computation, but it is available for per instance overrides because for some types, this might be done more efficiently.

skaller commented 2 years ago

FYI: Although the proposal for Interfaces is to use a where clause in a function interface, there is another way. In Felix, I have type classes with a where clause on functions too .. but I almost never bother to use it. Instead, I have this:

open[T] Eq[T];

That opens the type class (interface in Chapel) in the current scope. This does exactly the same thing as in a function: it injects the specialisation of the type class into the current scope. in the example, it injects the function for equality for all types. So you can use equality now in every function, even on variables whose type is a type parameter. Here's another example:

class Str[T] {
  virtual fun str: T -> string;
}
open Str;

Syntactic sugar, if you open a class without specialising the type variable, it is universally quantified. The str function can now be applied to every type. The user is encouraged to provide an instance for every type they define. In effect it is now a universal operator. So you do not need a where clause in a function that calls str even on variable with type being a function type parameter.

open is not transitive, the injected functions are not exported from a module. There is a transitive version inherit.

jabraham17 commented 2 months ago

This has had no activity in a while. I am closing this in favor of https://github.com/chapel-lang/chapel/issues/8629 which seems to be the main issue capturing our design for interfaces in the language