Interface Limitations in D
Elmar
chrehme at gmx.de
Sun Sep 19 20:00:11 UTC 2021
Hello D community.
This post is about `interface` design in D.
First, I see good additions compared to what I know from other
languages, mostly Java. Interfaces can be used to share function
implementations across multiple classes. It's very great that D
`interface`s can have pre- and post-conditions. This solves the
classic readability/comprehensibility problem of interfaces and
helps to implement and use interfaces in the intended way instead
of misusing interfaces in non-intended ways.
But I also see a few limitations compared with Java. This is not
a feature request. I'd only like to know what you think about
these 5 points or if you see design-reasons:
* **`interface`s don't permit runtime constants.**
Not limiting, only weird. Constants are stronly-pure
niladic (no-arguments) functions and D syntactically doesn't
differentiate between zero-argument function calls and variable
accesses. I could define a static niladic function with constant
return value in an `interface`. But the constant-member-syntax
```D
static immutable myVar = 0;
```
is easier to write.
Constants are no state. The purpose of an interface is to
describe API which is not supposed to change at runtime or during
software evolution while not fixing behaviour and ideally not
fixing future additions.
* **`interface`s do not permit overriding defaults**
D's interfaces don't allow default implementations of
static and non-static functions unlike Java. This makes class
implementations more verbose and promotes code duplication
because you need to repeat the default implementation in every
implementing class, even if you don't care.
Java uses the `default` keyword as attribute to allow for
overriding a default-implemented interface method.
Default methods are most important for the ability to
extend interfaces without requiring old classes to implement
newly added methods and without introducing stateful behaviour.
A partial workaround for the extensibility problem is the
definition of a new interface which also contains the old one.
If constant declaration syntax (as previously mentioned)
would be added to `interface`s, then runtime constants with
default value or without any value could be polymorphically
overridable within `class`es (overridden with a different
constant value). When they are overridable (e.g. using the
`default` keyword, opposite to `abstract`), they could be
syntactic sugar for niladic functions which return a constant
value and **optionally** could do a one-time-computation of the
return value when the static-constant-function is called the
first time.
Abstract classes are no replacement because, first, you
cannot inherit multiple abstract classes and, second, abstract
classes implement partial incomplete behaviour while interfaces
don't implement behaviour.
* **Non-polymorphic inheritance exists (`alias this` in `struct`s
or `class`es) but no non-polymorphic `interface`s for structs**
This one is most meaningful.
In my current project in D, I'm working on a low or
medium-low level and it's not suitable to use classes (they also
need to work in Better-C). I don't need polymorphy. I only like
to guarantee a **consistent** interface among my `struct`s. It
makes life of users easier and prohibits others from "inheriting"
my struct properties in unintended ways.
The current way of creating non-polymorphic `interface`s is
cumbersome: create a `mixin template` which mainly instantiates a
selfmade trait-template (a predicate) on `this` to check that the
environment implements certain function signatures. If I want to
use a non-polymorphic `interface` as a type, I'm using type
parameters and I use the trait-predicate in the `if`-constraint
of the templated entity.
It's more difficult to read and it's more verbose.
---
Sure, one could avoid explicit template notaton. A solution
would be a `static interface` (compile-time-dynamically
dispatched methods and constants) which represents a uniform
function-layout and a traits-predicate that is generated from the
`static interface` (or is at least simulated and implicitly
accessible via `is(type : interfaceName)`). Any implementing
types must satisfy this predicate.
Static `interface`s behave this way:
- When used as variable type, `static interfaces` would
behave like `auto` and a compile-time assertion of the associated
predicate. Any value can be assigned to such a variable as long
as it satisfies the predicate of the `static interface`.
Covariant types don't need to implement the `static interface`
explicitly to satisfy the predicate.
- When used as a function-parameter-type or member-type it
would be lowered to an implicit template parameter which is
checked against the generated predicate.
A lot of manual traits-checks using `static assert`s and
`if`-constraints could be simplified into just a typename, e.g.
when using Ranges.
---
A simpler variant would be a `mixin interface` (purely
static dispatch of methods) which only defines required constants
and functions to implement for a `class` or `struct` but which
cannot be used as a type otherwise except if there is a (default)
implementation of all functions. (No templates are created by
using the type.) `typeof(this)` would be allowed in `mixin
interface`. This essentially behaves like a `mixin template`
enhanced with "abstract" functions that must be implemented by
the implementing `class` or `struct`.
* **`interface`s can contain type definitions/declarations and
`alias` and can be overloaded, even though it's not documented.**
Runtime-constants are not permitted but surprisingly
compile-time constants are and `enum`-blocks and type
definitions. They even can be overloaded but *without polymorphy*
(dynamic dispatch). It seems this is not documented?
But you should be cautious what to document:
`alias`es can be useful if you'd want to change a function
name without breaking old code. Probably there is a way to
deprecate `alias`es with an annotation even.
My personal experience however is, that `alias`
declarations in `interface`s can be easily abused by using them
throughout the entire code of a class, unrelated to the interface
functions, which makes it very hard to find the declaration
manually.
Currently, one cannot do a lot against bad `alias`es in
`interface`s because limiting the scope or use of
`alias`-definitions in `interfaces` would be breaking old code.
But since it's undocumented, worst case breakage is reduced
(nobody said it's supported anyways, right?).
BTW, I see benefits in enabling the *pimpl pattern* within
interfaces.
```D
interface DoodadObtainable {
class Doodad; // opaque type of the pimpl pattern
Doodad obtainDoodad();
void releaseDoodad(Doodad);
}
class GadgetGizmo : DoodadObtainable
{
class Doodad
{
int foo;
this(int f) { this.foo = f; }
}
Doodad experienceCounter;
Doodad obtainDoodad()
{
return experienceCounter = new Doodad(13);
}
void doDoodadThings()
in (experienceCounter)
{
experienceCounter.foo++;
}
void releaseDoodad(Doodad d)
{
d.destroy();
}
}
```
An interface-implementation would then implement the
declared opaque type from the interface. The opaque type is
overriden polymorphically and internally represented by a generic
type like `void*` for classes or `void[]...` for structs. Since
the type-size is unknown, it wouldn't allow opaque value-types as
direct function-parameters, e.g. mere `struct`s; except when they
are treated like variadic arguments plus an implicit byte size
parameter for copying the data.
A default implementation of an opaque type declarations is
imaginable (with `default`) but without default implementation
(or: without an abstract constructor method in the `Doodad` class
example), opaque types cannot be instantiated outside of the
implementation class.
This pimpl pattern is a nice way to avoid template code
bloat and to avoid recompilation when something changes in opaque
types.
* **`interface`s can contain classes which violate the concept of
an interface.**
IMO, this is rather a limitation in persuing the purpose of
an `interface`. It can contain a `class` whose behaviour
specification defies the purpose of an `interface`. For Java it's
the same. If `interface` methods use specific classes, they also
can be defined outside of the interface, right? If you need
cohesion put them into the same file and make them private. I
usually use `template`s for controlling visibility and cohesion
and to avoid ugly nestings of definitions:
```D
template MyInterface() {
class MyParameter {
int something;
}
interface MyInterface {
interface MyInterfaceClass { // body behaves like
an interface itself
void tell(string);
string listen();
}
MyInterfaceClass do(MyParameter something);
}
}
```
*Just very annoying: you'd have to write `MyInterface!()`
everytime. It would be very useful, if you could omit the `!()`
if you'd like to pass zero compile-time arguments to it.*
And you can use `import` statements in `interface`s, if
it's located in another file.
For me it seems, classes or structs should not be in an
`interface` except when it is a default implementation that can
be overridden **polymorphically**.
Making rules more strict is not something, anyone would be
able to change without potentially breaking old code.
More information about the Digitalmars-d
mailing list