Thread overview
An example of typeclasses/traits/protocols/concepts in D with interfaces
Dec 23, 2020
sighoya
Dec 23, 2020
Adam D. Ruppe
Dec 23, 2020
sighoya
Dec 23, 2020
sighoya
Dec 23, 2020
sighoya
Dec 23, 2020
rikki cattermole
Dec 24, 2020
sighoya
December 23, 2020
```D
import std.stdio;

public interface Number(T)
{
    T add(T)(T n1,T n2);
    T sub(T)(T n1,T n2);
    T mul(T)(T n1,T n2);
    T div(T)(T n1,T n2);
}

public class DoubleNumber:Number!double
{
    static double add(double n1,double n2)
    {
        return n1+n2;
    }
    static double sub(double n1,double n2)
    {
        return n1-n2;
    }
    static double mul(double n1,double n2)
    {
        return n1*n2;
    }
    static double div(double n1,double n2)
    {
        return n1/n2;
    }
}


class IntNumber:Number!int
{
    static int add(int n1,int n2)
    {
        return n1+n2;
    }
    static int sub(int n1,int n2)
    {
        return n1-n2;
    }
    static int mul(int n1,int n2)
    {
        return n1*n2;
    }
    static int div(int n1,int n2)
    {
        return n1/n2;
    }
}

//Typeclasss instances
alias Instance(T:Number!int) = IntNumber;
alias Instance(T:Number!double) = DoubleNumber;


T squareCompiletime(T, alias Implementor = Instance!(Number!T))(T number)
{
    return Implementor.mul(number,number);
}

T squareRuntime(T)(T number,Instance!(Number!T) instance = new Instance!(Number!T))
{
    return instance.mul(number,number);
}

int main()
{
    writeln(squareCompiletime(3));
    writeln(squareRuntime(3));
    writeln(squareCompiletime(3.0));
    writeln(squareRuntime(3.0));
    return 0;
}
```

Some things that irk me:
1.) Can't we import aliases from other modules or am I just too dumb to see how?
2.) it would be nice to unscope the methods of a type, e.g. `T square(T, unscope alias Implementor = ...)` s.t. we don't have to prefix it all the time with Implementor, the same for the runtime version of square.

This should also work with operators, I've left them out here.

Clearly, language site implicit instance resolution is often more convenient, but D people like it more explicit. Moreover, language site instance resolution has to be implemented carefully to choose the appropriate instances with regard to the current scope (a.k.a global coherence problem) and most languages simply don't get that right, we can do it better, manually :).

I think it is also possible to require typeclass dependencies like in Rust, e.g. if A gets implemented then B also, or each Number must be Equatable:

`public interface Number(T) where Instance!(Eq!T)`
December 23, 2020
On Wednesday, 23 December 2020 at 14:20:39 UTC, sighoya wrote:
> 1.) Can't we import aliases from other modules or am I just too dumb to see how?

plain import includes aliases....

> 2.) it would be nice to unscope the methods of a type

that's what the `with` keyword does

T square(T, alias Implementor...) {
   with(Implementor) {
         // here
   }
}

December 23, 2020
On Wednesday, 23 December 2020 at 14:23:34 UTC, Adam D. Ruppe wrote:
> On Wednesday, 23 December 2020 at 14:20:39 UTC, sighoya wrote:
>> 1.) Can't we import aliases from other modules or am I just too dumb to see how?
>
> plain import includes aliases....
>

Strange, because importing alias Instance(T:Number!double) = DoubleNumber; doesn't work it complains with errors.


>> 2.) it would be nice to unscope the methods of a type
>
> that's what the `with` keyword does
>
> T square(T, alias Implementor...) {
>    with(Implementor) {
>          // here
>    }
> }

Nice, I wasn't aware of this, is akin to that possible for runtime objects?


December 23, 2020
The error was:
Error: template instance Instance!(Number!double) does not match template declaration Instance(T : Number!int)


December 23, 2020
On Wednesday, 23 December 2020 at 14:56:19 UTC, sighoya wrote:
> On Wednesday, 23 December 2020 at 14:23:34 UTC, Adam D. Ruppe wrote:
>> On Wednesday, 23 December 2020 at 14:20:39 UTC, sighoya wrote:
>>> 1.) Can't we import aliases from other modules or am I just too dumb to see how?
>>
>> plain import includes aliases....
>>
>
> Strange, because importing alias Instance(T:Number!double) = DoubleNumber; doesn't work it complains with errors.
>
>
>>> 2.) it would be nice to unscope the methods of a type
>>
>> that's what the `with` keyword does
>>
>> T square(T, alias Implementor...) {
>>    with(Implementor) {
>>          // here
>>    }
>> }
>
> Nice, I wasn't aware of this, is akin to that possible for runtime objects?

Ah nice, I see it, theats great!

December 24, 2020
I prefer the original name, signatures and to ditch the nastiness that is classes (which do everything wrong for best practice OOP).

https://gist.github.com/rikkimax/826e1c4deb531e8dd993815bf914acea#signatures
December 24, 2020
On Wednesday, 23 December 2020 at 23:21:47 UTC, rikki cattermole wrote:
> I prefer the original name, signatures and to ditch the nastiness that is classes (which do everything wrong for best practice OOP).
>
> https://gist.github.com/rikkimax/826e1c4deb531e8dd993815bf914acea#signatures

Could you elaborate a bit more about the signature details, when does an implementation (is it any kind of type except signature?) conform to the signature.

Conformity by structurality or by an indirect instance?

The former variant is known as structural typing, the best type system known for this is ocaml, and they already use the name `signature` for it, so it would already fit.
See: https://www.cs.cornell.edu/courses/cs3110/2020fa/textbook/modules/signatures.html

Note, there is an experimental feature to allow for structuralism with structs and interfaces
See: https://dlang.org/phobos/std_experimental_typecons.html

I want that too together with an operator opImplicitCoerce or opImplicitConvert (that was already proposed by someone else here) allowing implicit coercion/conversion when passing structs to callees expecting conforming interfaces.

An implicit operator is much more powerful than alias this which is coupled to an source type, the operator is decoupled.

I don't know what the state is with this extension and why it is over years still experimental...


The latter is known as typeclasses/traits/protocols. It is more powerul as a type can match even when their names doesn't equal the names in the typeclass, but the drawback is you have to pass an indirect impl to each function using typeclasses.
I have proposed a way to do it semi-automatically either by templates or by a default value s.t. not at every time at the callsite an implementation has to be passed into the callee.

Note, with an implicit conversion operator, we could also pass implicitly the typeclass instance in the conversion operation at the call site but that would ,sadly, only cover the non generic case.

I think that there is a big resistance in the D community regarding the introduction of new concepts, therefore reusing interfaces may be an acceptable way to go eventually by supporting to pass instances implicitly in the generic case, I don't know.

I think we would also need to enhance the overload resolution for concrete types s.t. the method is preferred which matches either exactly or there is an implicit order of conversions yielding the narrowest method, i.e. the method for which all types parameters of it are convertible in order to all other types of each method.

It would reduce ambiguity akin to Walthers fear about multiple inheritance a lot.
And if we have ambiguity we just add another method which intersects all the parameter types of all the methods in order if the types are interfaces or classes or any other existential, otherwise we have to add a method with an exact match.

It would require intersection types though, but I think we can construct them by templates?