What is the best way to program over "abstract" types in D?

mipri mipri at minimaltype.com
Sat Nov 23 13:17:49 UTC 2019


On Saturday, 23 November 2019 at 11:21:32 UTC, Ola Fosheim 
Grøstad wrote:
> ...

I read your question as "can I have typeclasses in D?"

I'll refer to typeclasses in a language other than Rust. You
don't need to know the language. I think you knowing Rust at
all might be clouding your vision, actually.

First, you have some definition of the typeclass:

   :- typeclass named(T) where [
       func name(T) = string
   ].

In English: a type T is a named(T) if there's a function
'name' that accepts a T and returns a string.

This definition might appear in a module with a bunch of
functions that operate on named(T), like

   :- func greeting(T) <= (named(T)).
   greeting(W) = "Hello, " ++ name(W).

This is a function that accepts a value of a type T (provided
that T is named) and then returns the concatenated string. This
looks an awful lot like OOP, but there's no runtime dispatch,
and there are no objects -- indeed, the type could be a
primitive type like int, or float. The exact 'name()' function
that gets called is statically determined.

Finally, there are declarations that some types are, in fact,
instances of the typeclass.

   :- type world ---> world.

   :- instance named(world) where [
       name(world) = "World"
   ].

   :- instance named(int) where [
       name(X) = "Anonymous Integer".
   ]. % (yes, all ints have the same name)

These might appear in completely different modules with their
own unrelated code, written by other people, at a later time,
and with this instance declaration, when the whole program is
compiled together, some integers or the 'world' abstract type
might be handled by a lot of code that only knows that it can
call some name() function to get a string. This code did not
need to be edited to add a 'world' case to a switch to provide
this functionality.

If you're thirsty when by a river you might drink from the
river. If you're thirsty when by a soda machine you might buy a
Coke. But rivers are not made of soda. People using FP
languages reach for typeclasses and people using OOP languages
reach for class hierarchies in the same kinds of situations,
even if there are enormous differences in the exact outcome.

One difference as you noted is that none of the name() calls
is virtual in the typeclass case.

So, what would you need for typeclasses in D? You need all the
same parts: a static declaration of some kind of typeclass, a
static restriction of functions to apply to the typeclass, and
static registration of types to the typeclass.

At least, you need all these parts if you want the same kind of
nice user interface, where invalid registrations are rejected
in a timely manner. Here's a hack, instead:

Typeclass declaration? There isn't one.

Maybe there's documentation:

   /+ Named(T)

      string name(T x);  // define me
   +/

Typeclass restriction? It looks for a magic member. You let
people into the building because they each came in with their
own badge and the badge looks right; you didn't consult a
filing cabinet containing descriptions of all the people
allowed in the building. You still accomplished the goal.
With some caveats: the filing cabinet might've said "cats can
come in too", but cats cannot show a badge. Likewise primitive
types like ints and floats can't be given static members.

   template isNamed(alias T) {
       enum isNamed = hasStaticMember!(T, "secretlyIsNamed");
   }

As used:

   string greet(T)(T x) if (isNamed!T) {
       return "Hello " ~ x.name;
   }

Typeclass registration? Add the member to your types. These
types have no other information to them:

   struct World {
     static char[0] secretlyIsNamed;
   }

   struct Bob {
     static char[0] secretlyIsNamed;
   }

And then of course, define the required functions:

   string name(Bob x) { return "Bob"; }

   string name(World x) { return "World"; }

And then you can call them with static dispatch:

   void main() {
     writeln(greet(World()));
     writeln(greet(Bob()));
   }


This all really works. You get your static polymorphism. The
compiler doesn't let you forget a name() function (if you try
to use it). You have generic code in a library that can be
reused with different types provided by users of the library.

But because it's a hack, there are some unwanted outcomes.
First, other parts of the language can see these
secretlyIsNamed members, and that might cause problems.
They're not really secret. Second, negative typeclasses are not
possible in FP langs, but we have them here:

   string greet(T)(T x) if (!isNamed!T) {
     return "Hello???";
   }

Third, not only can't we register primitive types into
typeclasses like this, we also can't register any type that
already exists or that's provided by third party code. In an FP
lang you could import a serialization module, import some kind
of special container from another module, and then in your own
code say, "special containers are serializeable too and this is
how they should be serialized", even though neither of the
module authors were aware of the other.

I'm sure that someone better at D could make enormous
improvements on this though, with non-intrusive type
registration and typeclass declarations that you can reflect
on, etc.

Here's a complete program:

   import std.stdio: writeln;
   import std.traits: hasStaticMember;

   template isNamed(alias T) {
       enum isNamed = hasStaticMember!(T, "secretlyIsNamed");
   }

   string greet(T)(T x) if (isNamed!T) {
       return "Hello " ~ x.name;
   }
   string greet(T)(T x) if (!isNamed!T) {
       return "Hello?";
   }

   struct World {
       static char[0] secretlyIsNamed;
   }

   string name(World x) { return "World"; }

   struct Alice { }
   string name(Alice x) { return "Alice"; }

   struct Bob {
       static char[0] secretlyIsNamed;
   }

   string name(Bob x) { return "Bob"; }

   void main() {
       writeln(greet(World()));
       writeln(greet(Alice()));
       writeln(greet(Bob()));
   }

Output:

   Hello World
   Hello?
   Hello Bob

https://godbolt.org/z/XgvfS3



More information about the Digitalmars-d-learn mailing list