DIP84

From D Wiki
Jump to: navigation, search

DIP84: Static Inheritance

Title: Static Inheritance
DIP: 84
Version: 1
Status: Draft
Created: 2015-10-30
Last Modified: 2016-05-9
Author: Atila Neves

Abstract

Express intent to the compiler that a user-defined type conforms to a static interface.


Rationale

Currently, D uses template constraints as a means of restricting the types a template function/struct/class can be instantiated with. Common examples are isInputRange!R and isOutputRange!(R, E). It is also common to statically assert that a user-defined type satisfies the template constraint predicate:

struct Struct
{
    //...
    static assert(isInputRange!Struct);
}


While this works, when the static assertion fails there is no help from the compiler to guide the developer to fix the error. This is in stark constrast to runtime polymorphism, where:

interface Interface
{
    void foo();
}

class Class : Interface
{
    //... no foo here
}

yields a compiler error message indicating that void foo() is not implemented. Similarly, it is not possible to instantiate a class with unimplemented abstract functions without getting an equivalent error.

This DIP attempts to make static polymorphism as easy to adhere to as the more well-known and established runtime variety. This is particularly important for D due to the emphasis on the former with respect to other languages.

Description

This DIP proposes a backwards-compatible change to the language so that it becomes possible for classes and structs to "inherit" from a template constraint predicate. Since classes can already inherit from interfaces and one class, the static keyword would be added to distinguish:

class Class : static isInputRange
{
    //...
}

Structs can't currently inherit and so wouldn't need static. However, it might be preferable to require it anyway for parity with classes:

struct Struct : isInputRange // or struct Struct : static isInputRange
{
   //...
}

Static inheritance would only be syntatically valid if the right-hand side of the colon is equivalent to a template of the form:

template Predicate(T) {
    enum Predicate = is(typeof(() { /*code*/ }));
}

The enclosed lambda would be allowed to have default arguments. Since templates must be visible, the compiler would have access to the code block inside the lambda. This is essential for the diagnostics.

In the case of multiple template arguments for the predicate, as is the case for isOutputRange, all but the first would be omitted for the static inheritance:

struct Struct: isOutputRange!(int)
{
    //...
}

And the predicate must have N + 1 arguments:

enum Predicate(T, U) { /*...*/ }

Implementation

Assuming that the static inheritance is syntatically valid as described above, the compiler would lower the struct/class definition from:

struct Struct: Predicate
{
    //...
}

to:

struct Struct
{
   //as before

   //added by the compiler:
   static if(!Predicate!(Struct))
   {
       auto failFunc(/* same args as lambda*/)
       {
           //insert code inside the lambda, substituting the types accordingly
       }
   }
}

In case the predicate has more than one template argument, it would go from:

struct Struct : Predicate!(T...)
{
    //...
}

to:

struct Struct
{
   static if(!Predicate!(Struct, T))
   {
       auto failFunc(/* same args as lambda*/)
       {
           //insert code inside the lambda, substituting the types accordingly
       }
   }
}

It would be best to change the line numbers reported in the resulting error messages to be changed to the line where the static inheritance is declared.

Alternatively, a new compiler trait __traits(compilesNoSuppress) could be used. It would work the same way as __traits(compiles) but would print out error messages when compilation failed. In that case, the first example would be lowered to:


static assert(__traits(compilesNoSupress, isInputRange!Struct));
struct Struct
{
    //as before
}

This has the immediate advantage of not requiring any line number changes.

Examples of lowering

For isInputRange, the original definition is shown followed by the lowering:

struct In: isInputRange { }

struct In
{
    static if(!isInputRange!(In))
    {
        auto failFunc(inout int = 0)
        {
            In r = In.init;
            if (r.empty) {}
            r.popFront();
            auto h = r.front;
        };
    }
}

For isOutputRange:

struct Out: isOuputRange!int { }

struct Out
{
    static if(!isOutputRange!(Out, int))
    {
        auto failFunc(inout int = 0)
        {
            Out r = Out.init;
            int e = int.init;
            put(r, e);
        }
    }
}

Improvements

Instead of relying on compilation errors from the code copied from the lambda, it should be possible to generate even better compiler errors for well-known types from the standard library.