Some comments on Interfaces after briefly reading https://chapel-lang.org/releaseNotes/1.24/04-ongoing.pdf
The "interfaces" thing is Haskell type classes. This is independent of parametric polymorphism. The way this works (in Felix at least) is that initially in phase 1 binding, you bind to a parametrically polymorphic interface. The constraints are not checked. During phase 2 binding as you're doing monomorphisation (elimination of type variables), you find and chose an instance, or barf if you can't find one. I can give more details on the resolution algorithms.
Now, interfaces are not properties of a single types. So given
interface Convertible(T1, T2) { .. }
how do you specify an instance? Can you write:
(int, real) implements Convertible
?
This looks messy. The constraint spec is OK like:
where (U1, U2) implements Convertible
but it would be cleaner to just do
where Convertible(U1, U2)
The shorthand:
?t:TotalOrder
is problematic if you also have kinding constraints. In fact, using ?t
inside a formula to introduce type variables is probably not so good either, it gets way too messy. What do you do if the type variable is not used in the formula? How do you introduce it?
In addition, when an interface has multiple functions in it, and you allow default implementations as Haskell and Felix do, you then must distinguish between a default implementation of an implementable function, and a helper function which cannot be overridden. In Felix I use the keyword virtual
to specify an implementable function. Since this concern also applies to associated types, I also use the keyword virtual
for associated types.
Finding of implementations from the general scope look very bad. You have to explicitly specify that a function is an implementation and what the types are like this:
implementation[K1] Convertible(K1, K1 * int) { ... }
Implementations can be polymorphic, and they can be specialised.
When you are calling a function, you need to be able to add a qualifier to specify which interface it comes from, along with type arguments. This is because, for example, the types are not used in the function domain and so cannot be deduced. It is important implementing the lookup that you find the interface and resolve the type arguments first before looking for an implementation. This makes the lookup a little bit different to ordinary lookup.
On interaction with ordinary generics. There are a couple of messy options here. In Felix there are three ways (more or less) to use an interface.
- Explicit qualification:
TotalOrder[int]::less(1,2)
Here the interface is named, and the type argument is specified. The ::
is from C++ notation. Uggh. Note, you STILL need to do overloading inside the interface to choose less, if you were stupid enough to provide two functions called less
in the interface. Probably this should be banned.
- By injection into a function scope, using
where
clause.
where Convertible(T1, T2)
When you do this, the functions in the interface are local to the function and therefore by the usual scoping rules hide functions from an outer scope. Which functions, if any, from the outer scope that you find depend on your normal hiding rules. In C++, the hiding is absolute which I consider a design bug, so in Felix, outer functions with the same name are still visible to an overload resolution if no function in the current scope matches. But Chapel should just use what rules it normally does for name hiding.
- By exposure with an
open
clause.
open[K] Convertible(K, K*int);
This puts the functions of interface Convertible into the current scope, with their domains constrained. In the common case
open[K1, K2] Convertible[K1,K2];
the constraints can be dropped:
open Convertible;
which opens the whole interface in the current scope. Here's an example from Felix:
class Str[T] { virtual fun str: T -> string; }
open Str;
instance Str[int] { fun str(x:int):string { .... }}
println(24.str);
where class is Chapel interface, instance is Chapel implements. When the user introduces a new type, they can add an instance of Str::str for that type.
There are more things you can do in Felix. For example you can "inherit" an interface into another interface to compose them. FYI here are some Felix interfaces for basic algebra stuff:
http://felix-lang.org/share/src/packages/algebra.fdoc
The most interesting is worth showing here:
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));
}
Notice the interface parameter M
is NOT a type but a type function! In Felix TYPE
is the kind of ordinary types, and so TYPE->TYPE
is a function from types to types. Also notice the join operator is not virtual, so it cannot be specified in an implementation. ret
and bind
are the traditional Haskell names for monad operators.
Whew. Sorry so long. I've implemented what you called interfaces so I know about the gotchas having been got by them. The biggest problem after you sort out the syntax is associated types. In languages like C++, Felix, and Chapel, it just doesn't play with ordinary overloading. In Haskell it's irrelevant, because there is no ordinary overloading, all overloading is inside a type class anyhow.
Also, I didn't explain fully, but you must specify the interface a function is providing an implementation for, otherwise polymorphic recursion won't work. Which opens up another issue: how are you going to terminate polymorphic recursions? Haskell knows how to check this. In Felix I just put a limit in the compiler because I couldn't figure out how to check it.
In case you don't know what polymorphic recursion is: it happens when, in an implementation of a function from an interface, you call the function from the interface again with a different type. So it isn't an ordinary recursive call.