Consistency, Templates, Constructors, and D3

Nick Treleaven nospam at example.net
Fri Aug 24 09:27:42 PDT 2012


On 24/08/2012 06:14, F i L wrote:
> 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).

> 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);

That syntax makes sense, but cast is a built-in language feature. I'm 
not sure making it look like a library function is really worth the 
change IMO. In some cases the syntax would be a bit noisier:

cast(immutable int) v;	// current
cast!(immutable int)(v); // new

> 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);

Or:
bool b = traits.hasMember!(Foo, "bar")();

Also:

int i;
bool b = __traits(isArithmetic, i);	// current
bool b = traits.isArithmetic(i);	// new

'i' cannot be a compile-time parameter or a runtime parameter either (by 
normal rules). So I think __traits are special, they're not really like 
a template function.

>      # 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.

Personally I think it's a benefit that D makes compile-time and runtime 
parameters look different in caller code. The two things are very different.

> 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))
>          ...
>      }

Perhaps I'm being pedantic, but here it would probably be:

void load(T:string)(T file){...}

void load(T:File)(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.

I think it's straightforward if you already know func<...>(...) syntax 
from C++ and other languages.

> 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.

The above code doesn't look much easier to understand than the current D 
example. I think knowing about template function syntax is simpler than 
knowing how to do static if tests.

I think there was a discussion about this before, but using:
void load(auto file)

Your analysis seems to go into more depth than that discussion AFAIR.

...
> Revisiting the cast()/__traits() issue. Given our new function call
> syntax, they would looks like:
>
>      cast(Type, value);
>      traits(CommandEnum, values...);

Those do now look nice and consistent.

> ######### CONSTRUCTORS ##########
>
> We're all aware that overriding new/delete in D is a depreciated
> feature.

I thought it was a deprecated feature (SCNR ;-))

> 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.

I have wondered whether 'new' will need to be a library function when 
allocators are introduced.



More information about the Digitalmars-d mailing list