Last update March 7, 2012

Typedef-Block



Typedef Block

A feature proposal for D -- EricAnderton

A means for extending a scalar type in a struct-like way that can:

  • maintain implicit casting to the underlying type
  • allow for operator overloads on scalar types
  • allow for static inheritance of methods when typedefs are 'inherited'
  • transparently expand the role of scalar types throughout the language

Syntax

The typedef statment is extended to support an optional class-like syntax. Note that the secondary form only allows for scalar types, since both structs, and classes have their own semantics for extension (inheritance etc).

typedef <type> <ident>;
typedef <scalar|enum|array|delegate|function> <ident>{
    <methods>
}

Any method signature is allowed, and any operator overload is possible. Constructor and Destructor syntaxes are allowed. The 'this' pesudo-member is also used throughout the typedef's methods, with the understanding that it is actually a pointer to the underlying type (scalar). Any members declared for the typedef must be static, as any non-static data would really make the type a struct not a scalar. The only valid, non-static member available is "this". The static member "init" is available to all typedefs and the static members "min" and "max" are available to non-array types.

Extending previously extended typedefs will force methods to be overloaded and overridden in a manner identical to class inheritance. Public, private and protected method types will be respected according to those rules.

The resulting type becomes a sort of static-class of methods that is only resolvable at compile time. The programmer must be aware that performing implicit or unsafe casts on such a typedef will result in changing the scalar's behavior to the behavior of the resulting type.

Motivation

The basic premise of extending typedef is to modify the behavior of scalars without having to re-implement a complete behavior type. If one wanted to add an attribute to an scalar type, or slightly modify the behavior of a scalar, all while still having the same behavior as the type it replaces, one would have to do no less than:

  • create a class or struct
  • completely emulate the behavior of the underlying type (arithmetic, casting and conversion)
  • implement the slightly divergent or enhanced behaviors, new attributes, etc
This list is deceptively short. There are over two-dozen operators in the D language, most of which apply to all scalar types. The remaining set apply to arrays. All this effort in scalar-type-emulation results in a large amount of code to be written to effectively emulate 90-99% of what is already provided in the scalar it replaces. The result is nothing less than code bloat and a needless duplication of effort.

Yet all this can be avoided if we can just change the parts we want to change. This is where the typedef block comes in.

Examples

By extending the notion of a typedef to include methods as well as just renaming the type, the language immediately becomes more flexible. Here are some examples.

typedef int SmartInt{
    public SmartInt opPos(){ // change opPos into an absolute-value operator
        if(*this < 0) return(-*this);
        return(*this);
    }
}

SmartInt foo = -6;
writefln("foo is: ",+foo); // outputs "foo is 6"

typedef int ClampedInt{ // value stays within range provided
    static this(){
        min = -100;
        max = 100;
    }
    private int clamp(int value){
        if(value > max) return(max);
        else if(value < min) return(min);
        else return(value);
    }
    public int opAssign(int value){
        *this = clamp(value); 
        return(this);
    }
}

ClampedInt foo = -200;
writefln("foo is: ",foo); // outputs "foo is -100"

typedef real FriendlyReal{
    static this(){
        init = 0; // use zero instead of NaN
    }
    public FriendlyReal opDiv(real value){
        if(value == 0) *this = real.NaN;
        else *this = *this / value;
        return(*this);
    }
}

FriendlyReal foo;
writefln("foo is: ",foo); // outputs "foo is 0"
writefln("foo/0 is: ",foo/0); // outputs "foo is NaN"

typedef void delegate()[] SimpleEvent{
    public void opCall(){
         for(void delegate() del; this){
             del(); // call the entire queue
         }
    }
}

void hello1(){ write("hello1 ");
void hello2(){ write("hello2 ");
SimpleEvent foo;
foo ~= &hello1;
foo ~= &hello2;
foo(); // outputs: "hello1 hello2";

Impact on D

The changes to the language needed to support an extended typedef may not be all that extensive. D already performs implicit casting and overload resolution for typedef and aliased types. Structs already enjoy the kind of compile-time class like method semantics that this would require, so that functionality already exists within the language. Also, the typedef statement has a syntax "hole" as there is no equivalent for the block-style declaration (using "{" and "}" ). This meshes perfectly with the overall context-free approach of D's design, and only serves to make it moreso.

Comments

A few comments on the first read-through:

  • how would name lookup actually work? The proposal is not that clear to me exactly what would happen to figure out what function to call.
  • opAssign? eek - that's a new operator for D and potentially very scary.
  • Is the FriendlyReal? example useful? Can't one set the init property of a standard typedef?
BenHinkle

Another variant

In my opinion typedef-blocks and structs are too similar to exists as different constructs. If we allow struct inheritance by means of just adding base struct members to the front of child struct, and allow struct inheritance from basic types it would solve the problem. Casting child struct to base struct can be allowed, but all struct methods must be treated as non-virtual, so we can keep compitability with C structs.

Examples:

// analog for typedef int MyInt
struct MyInt: int {}

//  ( super is just shortcat for cast(int*)this )
struct SmartInt: int {
    public SmartInt opPos(){ // change opPos into an absolute-value operator
        if(*super < 0) return(-*this);
        return(*this);
    }
}

// static array
//  ( using template syntax )
struct AutoSumIntArray(N): int[N] {
    private int _sum = 0;
    int sum() { return _sum; }
    int opIndexAssign(int val, int idx) {
        _sum -= (*super)[idx];
        (*super)[idx] = val;
        _sum += (*super)[idx];
    }
    int opSliceAssign(...) { ... }
}

// dynamic array
//   must use class syntax becouse of reference semantic
class BoundedIntDynArray: int[] {
    int bound = 100;
    this(int l) {
        if(l > bound) assert(0);
        super(l);
    }
    void length(int l) {
        if(l > bound) assert(0);
        super.length = l;
    }
}


FrontPage | News | TestPage | MessageBoard | Search | Contributors | Folders | Index | Help | Preferences | Edit

Edit text of this page (date of last change: March 7, 2012 20:33 (diff))