Consistency, Templates, Constructors, and D3

F i L witte2008 at gmail.com
Thu Aug 23 22:14:34 PDT 2012


DISCLAIMER: This isn't a feature request or anything like that. 
It's ONLY intended to stir _constructive_ conversation and 
criticism of D's existing features, and how to improve them _in 
the future_ (note the 'D3' in the title).

I've had a couple of ideas recently about the importance of 
consistency in a language design, and how a few languages I 
highly respect (D, C#, and Nimrod) approach these issues. This 
post is mostly me wanting to reach out to a community that enjoys 
discussing such issues, in an effort to correct any 
mis-conceptions I might hold, and to spread potentially good 
ideas to the community in hopes that my favorite language will 
benefit from our discussion.


---------------------------


First, let me assert that "Consistency" in a language is 
critically important for a few reasons:

1. One way of doing things means one way to _remember_ things. It 
keeps us sane, focused, and productive. The more we have to fight 
the language, the harder it is to master.

2. Less things to remember means it's easier to learn. First 
impressions are key for popularity.

3. Less discrepancies means fewer human errors, and thus, fewer 
"stupid" bugs.




######### CAST/TRAITS ##########

To start, let's look at: cast(T) vs to!T(t)

In D, we have one way to use template function, and then we have 
special keyword syntax which doesn't follow the same syntactical 
rules. Here, cast looks like the 'scope()' or 'debug' statement, 
which should be followed by a body of code, but it works like a 
function which takes in the following argument and returns the 
result. Setting aside the "func!()()" syntax for a moment, what 
cast should look like in D is:

     int i = cast!int(myLong);

It's a similar story with __traits(). What appears to be a 
function taking in a run-time parameter is actually compile-time 
parameter which works by "magic". It should look like:

     bool b = traits!HasMember(Foo);




######### FUNCTIONS PARAMETERS ##########

All that brings me to my next argument, and that's that the 
"func!()()" is inconsistent, or at the very least, hard to 
understand (when it doesn't have to be). We have one way of 
defining "optional" runtime parameters, and a different set of 
rules entirely for compile-time parameters. Granted, these things 
are very different to the compiler, to the programmer however, 
they "appear" to just be things we're passing to a function.

I think Nimrod has a better (but not perfect) approach to this, 
in that there are different "kinds" of functions. One that takes 
in runtime params, and one that takes in compile-time ones; but 
at the call site, you use them the same:

     # Nimrod code

     template foo(x:int) # compile time
       when x == 0:
         doSomething()
       else:
         doSomethingElse()

     proc bar(x:int) # run time
       if x == 0:
         doSomething()
       else:
         doSomethingElse()

     block main:
       foo(0) # both have identical..
       bar(0) # ..call signatures.

In D, that looks like:

     void foo(int x)() {
       static if (x == 0) { doSomething(); }
       else { doSomethingElse(); }
     }

     void bar(int x) {
       if (x == 0) { doSomething(); }
       else { doSomethingElse(); }
     }

     void main() {
       foo!0();
       bar(0); // completely difference signatures
     }

Ultimately foo is just more optimized in the case where an 'int' 
can be passed at compile time, but the way you use it in Nimrod 
is much more consistent than in D. In fact, Nimrod code is very 
clean because there's no special syntax oddities, and that makes 
it easy to follow (at least on that level), especially for people 
learning the language.

But I think there's a much better way. One of the things people 
like about Dynamicly Typed languages is that you can hack things 
together quickly. Given:

     function load(filename) { ... }

the name of the parameter is all that's required when throwing 
something together. You know what 'filename' is and how to use 
it. The biggest problem (beyond efficiency), is later when you're 
tightening things up you have to make sure that 'filename' is a 
valid type, so we end up having to do the work manually where in 
a Strong Typed language we can just define a type:

     function load(filename)
     {
       if (filename != String) {
         error("Must be string");
         return;
       }
       ...
     }

vs:

     void load(string filename) { ... }

but, of course, sometimes we want to take in a generic parameter, 
as D programmers are fully aware. In D, we have that option:

     void load(T)(T file)
     {
       static if (is(T : string))
         ...
       else if (is(T : File))
         ...
     }

but it's wonky. Two parameter sets? Type deduction? These 
concepts aren't the easiest to pick up, and I remember having 
some amount of difficulty first learn what the "func!(...)(...)" 
did in D.

So why not have one set of parameters and allow "typeless" ones 
which are simply compile-time duck-typed?

     void load(file)
     {
       static if (is(typeof(file) : string))
         ...
       else if (is(typeof(file) : File))
         ...
     }

this way, we have one set of rules for calling functions, and 
deducing/defaulting parameters, with the same power. Plus, we get 
the convenience of just hacking things together and going back 
later to tighten things up. We can have similar (to existing) 
rules for specialization and defaults:

     void foo(int x) // runtime
     void foo(x:int) // compiletime that must be 'int'
     void foo(x=int) // compiletime, defaults to 'int'
     void foo(x:int|string) // can be either int or string
     void foo(x=int|string) // defaults to int; can be string

as well for deduction:

     void foo(int x, T y, T)            { ... }
     void bar(int x, T y, T = float)    { ... }
     void baz(int x, T y, T : int|long) { ... }

     void main()
     {
       foo(0, "bar");      // T is string
       foo(0, 1.0);        // T is double
       foo(0, 1.0, float); // T is float
       bar(0, 1.0);        // T is float (?)
       baz(0, 1.0);        // error: needs int, or long
     }


Revisiting the cast()/__traits() issue. Given our new function 
call syntax, they would looks like:

     cast(Type, value);
     traits(CommandEnum, values...);

Now, I'm sure everyone is saying "What about Type template 
parameters? How do we separate them from constructor parameters?" 
Please keep reading.



######### CONSTRUCTORS ##########

We're all aware that overriding new/delete in D is a depreciated 
feature. I agree with this, however I think we should go a step 
further and remove the new/delete syntax all together... :D crazy 
I know, but hear me out.

We replace it with special factory functions. Example:

     class Person {
       string name;
       uint age;

       this new(string n, uint a) {
         name = n;
         age = a;
       }
     }

     void main() {
       auto philip = Person.new("Philip", 24);
     }

Notice 'new()' returns type 'this', which makes it static and 
implicitly calls allocation methods (which could be overridden) 
and has a 'this' reference.

This way, creating objects is consistent across struct/class and 
is always done through a named function. So things like 
converting can become arbitrarily consistent through a naming 
convention:

for example, we could use 'from()' in replace of to()/cast():

     auto i = int.from(inputString);
     auto i = int.from(myLong);

and we don't have to fight for overloads, or have to remember 
special factories (for things like FreeLists) when we usually 
think to use the 'new T()' syntax:

     // Naming clarifies intention
     auto model = Model.new(...);
     auto model = Model.load("/path/to/file");

Desctructors would be named as well, so we could force delete 
objects:

     struct Foo {
       this new() { ... }
       ~this delete() { ... }
     }

     void somefunc() {
         auto foo = Foo.new();
         scope(exit) foo.delete(); // forced
     }

This would also keep consistent syntax when using 
FreeLists/MemeoryPools, because everything is done through 
factories in this case, and the implementation can be arbitrary:

     class Foo {
       private Foo _head, _next;

       this new() @noalloc {
         if (_head) {
           Foo result = _head;
           _head = _head.next;
           return result;
         }
         else {
           import core.memory;
           return cast(Foo, GC.alloc(this.sizeof));
         }
       }

       ~this delete() {
         ...
       }
     }


More importantly, it's keeps Type template parameters from 
conflicting with constructor params:

     struct Foo(T) {
         this new(T t) { ... }
     }

     alias Foo(float) Foof;

     void main() {
         auto foo = Foo(int).new(123);
         auto foof = Foof.new(1.0f);
     }



With these changes, the language is more consistent and there's 
no special syntax characters or hard to understand rules (IMO).

Thanks for your time. Please let me know if you have any thoughts 
or opinions on my ideas, it is after all, why I'm posting them. :)


More information about the Digitalmars-d mailing list