Skip to content

Instantly share code, notes, and snippets.

@rikkimax
Last active June 26, 2019 17:53
Show Gist options
  • Save rikkimax/41f67550fbb2eacb858e7bfdb95bcdf0 to your computer and use it in GitHub Desktop.
Save rikkimax/41f67550fbb2eacb858e7bfdb95bcdf0 to your computer and use it in GitHub Desktop.

TODO: some form of introductory paragraph about what goes under this

Requirements

Use cases

Semantics

TODO: big picture stuff

There are two sets of choices available to the D community for this DIP. The first is the reordering of arguments with a choice between full, partial or no reordering. Partial is the default of the three and provides a transitional path to full reordering.

The second set of choices is for parameter syntax. A choice of either an attribute or a syntax based upon angle brackets. Both have benefits but because of some potential implementation constraints the attribute is the default.

To resolve and verify named arguments to named parameters the resolution algorithms are modified by appending some new logic on to the existing algorithms. The logic is the same for both functions and templates but requires hooking into the existing logic of parameter to argument confirmation.


Named parameters may have a default value associated with it. If it is not provided at the declaration site it must be provided in the arguments list. The determinance of the validity of a template initiation or function call for default values is done by the respective resolution algorithm addition listed in the Resolution section.

Named arguments specification is the same for both functions and templates. It takes the form of Identifier : Argument where Argument is the respective rule. See Argument Syntax section for further information.

TODO: link^


A named parameter may appear in the parameter list after a variadic parameter. A named argument terminates arguments passed to variadic parameter. I.e.

void func(int[] args..., @named bool flag);

func(1, 2, flag: false); // ok
func(1, 2, flag: true, 3); // error: unexpected token '3' expecting ')'

This also applied to named template parameters. I.e.

template Foo(T, U... @named V) {}

Foo!(int, float, string, V: bool); // ok
Foo!(int, float, V: bool, string); // error: unexpected token 'string' expecting ')'

Overload resolution for symbols (for functions and templates declarations) is done without named parameters. They are ignored. Ignoring named parameters in both cases means that the relevent algorithms and user code does not need to change to accomedate named parameters but they must be modified to filter them out. Giving the same behavior as currently. A side effect of this is that name mangling does not need to change to accomedate these new parameters.

An example of code that will not compiled is the following snippet:

void foo(int a) {
}

void foo(int a, @named int b) {
}

struct Bar(T) {
}

struct Bar(T, @named int Flag) {
}

Both foo and Bar examples are equal in showing that overload resolution does not take into account named parameters and ignroes them.

The respective usage (which won't compile as well):

foo(1); // error: matches both declarations
foo(1, b: 2); // error: matches both declarations

alias Bar1 = Bar!int; // error: matches both declarations
alias Bar2 = Bar!(int, Flag: 0); // error: matches both declarations

Templated declarations (struct, class, union, template block, mixin template) that have named parameters expose the named parameter as a member of the type.

struct MyType(@named SomeType) {
}

static assert(is(MyType!(SomeType: int) == SomeType));

The exposure of named parameters as a member of a type replaces existing code that have previously been done with an alias and a different template parameter name. The behavior with eponymous templates is that a named parameter must not match the name of the declaration. The below code will not compile and is the basis for this decision:

template Foo(Foo) {
}

pragma(msg, Foo!int.Foo);

Manifest enum's and templated alias's are treated as if they were eponymous templates and do expose named parameters as members.


It is an error to name symbols + variables the same name as a named parameter. I.e.

A template block:

template Foo(@named Bar) {
  struct Bar {} // error: named template parameter collides with declaration
}

A function body:

void myFunction(@named Input)(@named Input value) {
    int value; // error: named function parameter collides with declaration
    
    alias Input = Something; // error: named template parameter collides with declaration
}

Currently the D programming language does not have the facilities to inspect template declarations for their parameters. If you know the parameter count and if it is a value or a type, you can access to it via the is expression. In examples this variant of the is expression is typically used inside of static if conditions. Where the body is valid only if the initialized template does have a given set of arguments as provided to the is expression.

To prevent the complication of the is expression for template instances to get access to named arguments the usage of a new trait getNamedParameters is recommended (see Supporting Syntax for further details). To get access to the argument use the name and the getMember trait.

TODO: link^

Function parameters, unlike template parameters are inspectable. It is possible to get storage classes, types and names of them. This uses a second variant of the is expression with the __parameters specialization. However it does not return a tuple that can be modified to accomedate more information. Further a new trait isNamedParameter is added as a shortcut to detect if a given parameter is a named parameter.

Resolution

To resolve a named argument(s) against named parameters the same addition is applied to both functions and template initiations. They differ on how to determine if an argument matches the parameter but not how to determine which parameter goes with which argument.

The below process is appended to each resolution algorithm as appropriete.

  • For each named argument:
    • If the named argument name has already been processed in this argument list, error.
    • If named arguments name is found in the named parameter list (sublist of all parameters):
      • Positional argument checks go here (see reordering).
      • Confirm that the argument can be passed to parameter, if not error.
    • Else:
      • Error.
  • For each named parameter:
    • If it does not have a default value or a corresponding named argument, error.

The previous resolution(s) algorithms remain unchanged, but may require refactoring to only account for unnamed arguments.

Argument Syntax

One new syntax is added to match a named argument to a named parameter. It is split into two to cover templates and function calls. The syntax is based upon DIP88's, using the form of Identifier : Argument. Where Argument matches to the respective argument type and Identifier matches the named parameter name.

The order of evaluation of named function arguments occur in order of the arguments list (full, not subset). The order of passing of named function arguments (relative to other named function arguments and to unnamed) is platform defined. No modification to name mangling is in this DIP but may be ammended if it is a platform implementation or safety issue.

Equivalent example of function and delegate usages:

void myFunction1(@named int x, @named int y) {
}

myFunction1(x: 10, y: 10);

void myFunction2(@named T)(@named T x, @named T y) {
}

myFunction1!(T: float)(x: 10, y: 10);

Example usage using a mixin template:

mixin template Foo(T, @named ErrorType:Throwable) {
  
}

mixin Foo!(bool, ErrorType: Error);

The grammar that is being added:

TemplateArgument:
+   NamedTemplateArgumentList

+ NamedTemplateArgumentList:
+     NamedTemplateArgument
+     NamedTemplateArgument ,
+     NamedTemplateArgument , NamedTemplateArgumentList

+ NamedTemplateArgument:
+     Identifier : TemplateArgument

+ NamedArgument:
+    Identifier : ConditionalExpression

Reordering

Parameter Syntax

Supporting Syntax

Two new traits are added to support type inspection. The first addition is isNamedParameter which handles checking against a function, delegate or a function pointer if a given parameter index is a named parameter with respect to the is expression __parameters specialization. The second addition is getNamedParameters which returns a list of identifiers for a given declaration, type, function, delegate or a function pointer which are named parameters.

Example usage of isNamedParameter trait with respect to a function.

void myFunction(@named int a) {
}

static if (is(typeof(func) Params == __parameters)) {
    static foreach(i, Param; Params) {
        pragma(msg, Param); // int
        pragma(msg, __traits(identifier, Params[i .. i+1])); // a
        pragma(msg, __traits(isNamedParameter, typeof(func), i)); // true
    }
}

The trait isNamedParameter has the same signature as getParameterStorageClasses. The description from the specification is:

Takes two arguments.
The first must either be a function symbol, or a type that is a function, delegate or a function pointer.
The second is an integer identifying which parameter, where the first parameter is 0.
It returns a tuple of strings representing the storage classes of that parameter.

The difference between the traits isNamedParameter and getParameterStorageClasses is that isNamedParameter returns a boolean on if the parameter is a named parameter or not.

Example usage of getNamedParameters trait with respect to a function pointer and struct declaration.

struct MyType(@named Info) {
}

pragma(msg, __traits(getNamedParameters, MyType)); // tuple("Info");
pragma(msg, __traits(getNamedParameters, MyType!(Info: int))); // tuple("Info");

alias MyFunction = void function(@named bool myFlag);

pragma(msg, __traits(getNamedParameters, MyFunction)); // tuple("myFlag");

The additional grammar:

TraitsKeyword:
+    isNamedParameter
+    getNamedParameters

As a language feature, named arguments touches upon function overloading and symbol resolution algorithms. The part of the function overloading resolutions that handles parameters via a list of levels has been described by Andrei Alexandrescu during his DConf talk in 2019 as "Changing this list would be a major hurdle". As of writing of this DIP, symbol resolution algorithms have not been described in the spec.

For all of the above reasons, this DIP will not affect function overloading or symbol resolution for present code, to prevent significant complications in the design.

This has the side effect that named parameters must be opt-in. They must be marked as such, otherwise there will be no way to determine which overload to resolve to, if named arguments can be optionally set.

The requirements that have been described so far are:

  1. Named parameters must not modify symbol or overload resolution. They may and should be verified post overload resolution has completed.
  2. Each named parameter must be marked using e.g. an attribute to distinguish it from an unnamed parameter.
  3. In terms of naming of parameters, function and templated parameters are equivalent for syntax. But they may differ on semantics.
  4. They can be optional in an arguments list except where there is no defaulting value.

The syntax chosen in this DIP differs from immediate expectations because of existing language semantics primarily AssignExpression.

The language rule of AssignExpression is problematic because this is the type associated with function parameters. Allowing them to accept Identifier = Expression. Which is the same syntax that would be expected for named arguments in other c family languages. Instead of complicating the language with exceptions to this existing semantic, another syntax was chosen.

The requirement produced to handle the syntax of a named argument is:

  • The argument syntax must not use the form Identifier = Expression. But it must still work correctly and as expected with the AssignExpression rule.

Many languages support named arguments for function parameters in varying forms. But no language known to the author that is mainstream supports named arguments for template parameters. In D a sentiment is that templates are an evolution upon function parameters. So supporting it for both is a natural requirement.

Copying a language feature from another part and including it somewhere else may seem like a good idea, but that does not mean that it is valid. The initial behavior would be to allow setting a template parameter by using a name and only this. This would have the same and equal semantics as to function parameters being called. But unlike functions without extra state added, types can have their template parameters once initiated inspected by outside parties.

Accessing templated types template arguments is done by using the is expression. The is expression has been in the past (has since been mitigated) expressed as being heavily overloaded and doing too much. Fundamentally a template parameter is acting as a hidden attribute of a type that requires special syntax to get access to. This is not required and could be construed as bad language design.

The requirements for templated named arguments are:

  1. They must be available to be accessed externally from the type as well as internally.
  2. They do not need to be accessible from the is expression, see 1 for what to use instead.
  3. They must not be private to the type. If you can access the symbol, you can access a named parameter.

Evaluation of existing D strategies

To access template parameters once a declaration has been initialized, the is expression is used.

To access function parameters the usage of the is expression combined with the __parameters specialization. For further information such as storage class on a parameter, traits is used.

Requirements related to the introspection of the language:

  1. The is expression should not be able to lookup the named template parameters, otherwise it is ambiguous for unnamed.
  2. Using the is expression with the __parameters specialization to get a function parameters requires a trait to get information such as identifier or storage classes.
  3. A trait to be able to get all named parameters names for both functions and templates.

Reordering

Named argument reordering is a defining feature of named arguments. It allows placing the argument which matches a parameter anywhere in the argument list. There have been many different designs for this portion of the language feature that it warrents a choice. None of options propose restrictions to where a named parameter may be in the parameter list.

The three options chosen are partial, full and no reordering. No reordering is akin to Objective-C's "interleved arguments" behavior, contrastly Ada is an example of full reordering. Lastly the default is partial reordering has been chosen which is both more restrictive and less restrictive than other mainstream languages.

Full

Full reordering allows a named argument to be positioned anywhere in the argument list (of templates or functions). The location does not need to match to the parameter list or its position relative to other arguments.

No semantics or syntax is added to support this option.

void myFunction(@named int x, @named int y, int v);

myFunction(x: 1, y: 2, 0); // success
myFunction(0, y: 1, x: 2); // success

myFunction(0, 0, 0); // failure
Pros Cons
No restrictions Can be abused heavily with mixed domain objects vs arguments
Easier to remember syntax & semantics
Less code to implement (so faster, less memory used)

Partial

Partial reordering of named arguments adds some restrictions to the location that a named argument may appear in an arguments list. This option proposes a single restriction in that the named argument list must match the named parameter list ignoring the unnamed arguments/parameters.

void myFunction(int w, @named int x, @named int y = 0, @named int z);

myFunction(0, x: 6, y: 7, z: 8); // success
myFunction(x: 6, z: 7, 0); // success

myFunction(y: 6, x: 7, z; 8); // failure
myFunction(6, 7, 8); // failure

The semantic added (which is added to the resolution algorithm):

  • If there is a prior named argument and it does not appear in the named parameters list prior to this one, error.

The semantic used by C# and Python is:

  • If there is an unnamed argument after a named argument, error.

These can be combined to create a relaxed restriction that is inbetween the restrictive partial reordering that this option provides and full reordering:

  • If there is an unnamed argument after a named argument:
    • If there is a prior named argument and it does not appear in the named parameters list prior to this one, error.

This revised semantic can be used as a transitional stage between partial reordering and full reordering depending upon the experience gained from using the proposed semantic.

Pros Cons
Not for use by domain objects so they cannot be seperated into seperate arguments Cannot create domain objects as arguments without abusing the language feature
Partial reordering is quite common Not this variant
Little to no breakage if this choice is wrong This may not be the final semantic
Provides a path to relax the restrictions as experience is gained

None

No reordering of named arguments adds a very restrictive semantic to where a named argument may appear in the arguments list. The argument list must match the parameter list irregardless of naming.

void myFunction(@named int x = 0, @named int y);

myFunction(x: 6, y: 7); // success
myFunction(y: 7); // success
myFunction(y: 6, x: 7); // failure
myFunction(6, 7); // failure
void myFunction(int x, @named bool myFlag = false, int y = 0);

myFunction(0, myFlag: true, 7); // success
myFunction(0, myFlag: true); // success
myFunction(0); // success
myFunction(0, 2); // success

The semantic added (which is added to the resolution algorithm):

  • If the named argument position in the arguments list does not match the position of the matched named parameter in the template list, error.
Pros Cons
Long argument lists are easier to understand
Cannot be used for anything other than identification of an argument
Not suitable as an option for this DIP as the name is required for a named parameter to set it

Parameter Syntax

The D programming language community has come up with multiple syntaxes for opt-in named arguments over the years which differ from other languages. DIP88 uses a colon to donate a named parameter, DIP1019 originally proposed the use of the named attribute and this DIP introduces a new one, the angle bracket syntax.

The syntax that this DIP presents prevents clashes with other existing language features like template parameters (T:V or T=int). It aims to be opt-in, rather than opt-out to prevent clashes with core semantics of the D programming language like function overloading.

The syntaxes chosen for this DIP is the named attribute and the angle bracket syntax. A less powerful syntax was added in the form of an attribute (default) was added because of less desirable aspects of the angle bracket syntax which may cause problems in the implementation of this DIP.

@named Attribute

This option adds a new parameter attribute which marks a parameter as named. This is a per attribute syntax that opt-in's a parameter. It is similar to how other languages like Python work, but in attribute form. I.e.

struct Map(@named alias Function, @named InputType, @named ElementType) {
   InputType input;

   this(InputType input) {
      this.input = input;
   }

   @property {
       ElementType front() {
           return Function(input.front);
       }

       bool empty() {
           return input.empty;
       }
   }

   void popFront() {
       input.popFront;
   }
}

There are no new semantics associated with this syntax.

Pros Cons
Simple addition Will make parameter lists longer
InOut:
+   @ named

TemplateParameter:
+   @ named TemplateParameter
    ...

Angle brackets

The angle bracket syntax adds a syntax that wraps the parameters within an open + close pair of angle brackets (< and >). This donates that the enclosing parameters are to be named.

One new semantic is added for this syntax. The ability to omit the outer template parameter curved brackets when only named parameters exist on a class, union, struct, template, mixin template. I.e.

struct Map<alias Function, InputType, ElementType> {
   InputType input;

   this(InputType input) {
      this.input = input;
   }

   @property {
       ElementType front() {
           return Function(input.front);
       }

       bool empty() {
           return input.empty;
       }
   }

   void popFront() {
       input.popFront;
   }
}
Pros Cons
Can mark multiple parameters in bulk Has to use the angle brackets (only open+close brackets unused on a US-101 keyboard for declarations/types)
Can be used without curved brackets to donate template for structs/classes/unions/template/mixin templates May require token lookahead to implement angle brackets (expressions)
TemplateParameters:
+    < NamedTemplateParameterList|opt >

TemplateParameter:
+    < NamedTemplateParameterList|opt >

+ NamedTemplateParameterList:
+    TemplateParameter
+    TemplateParameter ,
+    TemplateParameter , NamedTemplateParameterList

Parameter:
+   < NamedParameterList|opt >

+ NamedParameterList:
+    Parameter
+    Parameter ,
+    Parameter , NamedParameterList

Use cases

An example from std.net.isemail which has a flag argument called checkDNS that previously used std.typecons's Flag.

EmailStatus isEmail(Char)(const(Char)[] email, EmailStatusCode errorLevel = EmailStatusCode.none
  @named bool checkDNS = false)
  if (isSomeChar!Char);

Another example but from std.string which has the flag called cs. It too previously used std.typecons's Flag type.

ptrdiff_t indexOf(C)(scope const(C)[] s, dchar c, size_t startIdx,
  @named bool caseSensitive = true)
  if (isSomeChar!C);

Previously the above example used the name cs in place of caseSensitive. A two letter name of an argument does not properly explain the purpose of the argument.


A pattern of code that is common in D code bases that heavily use meta-programming, is to use an alias to expose template parameters via a renamed template parameter.

struct Foo(T_) {
  alias T = T_;
}

A simplified real world example that originates from AsumFace on IRC is shown below.

import std.traits : isInstanceOf;

struct BasicPriorityQueue(T, ulong maxPriority_)
{
    alias maxPriority = maxPriority_;
    T[][maxPriority + 1] queue;
    
    void insert(A)(A arg)
        if (isInstanceOf!(PriorityItemData, A) && A.priority <= maxPriority)
    {
        
        queue[A.priority] ~= arg.payload;
    }
}

auto PriorityItem(ulong priority, T)(T payload)
{
    return PriorityItemData!(priority, T)(payload);
}

private struct PriorityItemData(ulong priority_, T)
{
    alias priority = priority_;
    T payload;
}

When converted to named parameters:

import std.traits : isInstanceOf;

struct BasicPriorityQueue(T, @named ulong maxPriority)
{
    T[][maxPriority + 1] queue;
    
    void insert(A)(A arg)
        if (isInstanceOf!(PriorityItemData, A) && A.priority <= maxPriority)
    {
        
        queue[A.priority] ~= arg.payload;
    }
}

auto PriorityItem(ulong priority, T)(T payload)
{
    return PriorityItemData!(T, priority: priority)(payload);
}

private struct PriorityItemData(T, @named ulong priority)
{
    T payload;
}

To create a core.time : Duration the usage of a template called dur is used in examples of Phobos. But this requires initiating a template with the time unit which is not easily auto-completed for IDE's.

Named parameters can provide an alternative interface to templated functions where all options are known at compile time and each option only differs in an operation to provide with all options compatible with each other in some way.

import core.time;
import std.typecons;

Duration dur(Nullable!Duration base = Nullable!Duration(),
              @named long hours = 0,
              @named long minutes = 0,
              @named long seconds = 0) {
    return base.get(dur!"hnsecs"(0)) + hours.dur!"hours" + minutes.dur!"minutes" + seconds.dur!"seconds";
}

writeln(dur(hours: 1,  minutes: 2, seconds: 3)); // 1 hour, 2 minutes, and 3 secs

The following function signature is a counter-example to a named arguments language feature.

void blit(Image source, Image destination, Rect sourceRect, Rect destinationRect);

The function copies an image (or sub image) into another image (or sub image). This is a common operation in graphics libraries.

Another form that this signature can take is to expand the Rect parameters into offset and size points.

Converted to use named arguments the signature is shown below.

void blit(@named Image source, @named Image destination,
  @named Rect sourceRect = Rect(-1, -1, -1 -1),
  @named Rect destinationRect = Rect(-1, -1, -1, -1));

As a counter example the above signatures do not show the invalidity of the preposition of using named arguments as the solution. A language feature that D supports is Unified Function Call Syntax (UFCS). This feature combined with a different abstraction influenced from another D language feature (slices), allows for the signature to become:

void blit(Image source, Image destination);

Equivalent behavior of blit function between the original and the simplified abstraction:

source.subset(Rect(-1, -1, -1, -1)).blit(destination.subset(Rect(-1, -1, -1, -1)));

TODO: Alternative design for ranges

struct Adder<SourceType, Type=SourceType.Type> {
    SourceType source;
    Type toAdd;

    @property {
        Type front() {
            return source.front() + toAdd;
        }

        bool empty() {
            return source.empty();
        }
    }

    void popFront() {
        source.popFront();
    }
}

auto adder(Source)(Source source, Source.Type toAdd) {
    return Adder!(SourceType: Source, Type: Source.Type)(source, toAdd);
}

TODO: design for logging:

Logging functions can be a problem to model because of the need to pass additional information from the compiler without specifying it on the caller side. This additional information is different to other arguments which are passed explicitly. Default arguments may be passed representing the source location of the function call,but should generally not be specified by the developer except in advanced use cases.

```d
void log(T...)(T args, string moduleName = __MODULE__, string functionName = __Function__, size_t lineNumber = __LINE__) {
    writeln(moduleName, ":', functionName, "[", lineNumber, "] ", args);
}

Named parameters can be used to visually separate parameters whose arguments should rarely be provided in the function call from those that should generally be provided.

void log(T...)(T args, <string moduleName = __MODULE__, string functionName = __FUNCTION__, size_t lineNumber = __LINE__>) {
    writeln(moduleName, ":', functionName, "[", lineNumber, "] ", args);
}
@rikkimax
Copy link
Author

a template parameter is a part of a type. when you construct a type, you don't have it already. so, you speak about template parameter as part of declaration

@rikkimax
Copy link
Author

Overloading!!!

@rikkimax
Copy link
Author

rikkimax commented Jun 24, 2019

Any number of named parameters may follow the variadic parameter in the parameter lists of variadic functions and templates, but they explicitly cause the variadic argument list in the function call or templated instantiation to terminate; i.e. any number of arguments may be passed before any subsequent named argument, but none may follow. The following code is valid and shouldFree terminates the variadic argument list.

void func(T..., <alias FreeFunc>)(T t, <bool shouldFree=false>) {
}

func!(FreeFunc : someRandomFunc)(1, 2, 3, shouldFree : true);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment