Skip to content

Specify opApply as an alias to a function template instance behavior #3859

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
250 changes: 182 additions & 68 deletions spec/statement.dd
Original file line number Diff line number Diff line change
Expand Up @@ -698,88 +698,100 @@ foreach (string s, double d; aa)

$(H3 $(LNAME2 foreach_over_struct_and_classes, Foreach over Structs and Classes with `opApply`))

$(P If the aggregate expression is a struct or class object,
the $(D foreach) is defined by the
special $(LEGACY_LNAME2 opApply, op-apply, $(D opApply)) member function, and the
`foreach_reverse` behavior is defined by the special
$(P If the aggregate expression is a `struct` or `class` object,
iteration with $(D foreach) can be defined by implementing the
$(LEGACY_LNAME2 opApply, op-apply, $(D opApply)) member function, and the
`foreach_reverse` behavior is defined by the
$(LEGACY_LNAME2 opApplyReverse, op-apply-reverse, $(D opApplyReverse)) member function.
These functions must each have the signature below:
)

$(GRAMMAR
$(GNAME OpApplyDeclaration):
`int opApply` `(` `scope` `int delegate` `(` $(I OpApplyParameters) `)` `dg` `)` `;`
`int opApply` `(` `scope` `int delegate` `(` $(I OpApplyParameters) `)` `body` `)` `;`

$(GNAME OpApplyParameters):
*OpApplyParameter*
*OpApplyParameter*, *OpApplyParameters*
$(GLINK ParameterDeclaration)
$(GLINK ParameterDeclaration), *OpApplyParameters*

$(GNAME OpApplyParameter):
$(GLINK ForeachTypeAttributes)$(OPT) $(GLINK2 type, BasicType) $(GLINK2 declaration, Declarator)
$(GLINK ParameterStorageClass)$(OPT) $(GLINK2 type, BasicType) $(GLINK2 declaration, Declarator)
)

$(P where each $(I OpApplyParameter) of `dg` must match a $(GLINK ForeachType)
in a $(GLINK ForeachStatement),
otherwise the *ForeachStatement* will cause an error.)
Comment on lines -721 to -723
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep this paragraph too (though it would need to use ParameterDeclaration if you reinstate that change).


$(P Any *ForeachTypeAttribute* cannot be `enum`.)

$(PANEL
To support a `ref` iteration variable, the delegate must take a `ref` parameter:

$(SPEC_RUNNABLE_EXAMPLE_COMPILE
---
struct S
{
int opApply(scope int delegate(ref uint n) dg);
uint n;
int opApply(scope int delegate(ref uint) body) => body(n);
}
void f(S s)
void main()
{
S s;
foreach (ref uint i; s)
i++;
{
i++; // effectively s.n++
}
foreach (i; s)
{
static assert(is(typeof(i) == uint));
assert(i == 1);
}
}
---
)
Above, `opApply` is still matched when `i` is not `ref`, so by using
a `ref` delegate parameter both forms are supported.
)
Above, `opApply` is matched both when `i` is and is not `ref`, so by using
a `ref` delegate parameter, both forms are supported.

$(P There can be multiple $(D opApply) and $(D opApplyReverse) functions -
one is selected
by matching each parameter of `dg` to each $(I ForeachType)
declared in the $(I ForeachStatement).)
Stating the type of the variable like in the first loop is optional,
as is showcased in the second loop, where the type is inferred.
)

$(P The body of the apply
function iterates over the elements it aggregates, passing each one
in successive calls to the `dg` delegate. The delegate return value
determines whether to interrupt iteration:)
$(P The apply
function iterates over the elements it aggregates by passing each one
in successive calls to the `body` delegate. The delegate return value
of the `body` delegate
determines how to proceed iteration:)

$(UL
$(LI If the result is nonzero, apply must cease
$(LI If the result is nonzero, the apply function must cease
iterating and return that value.)
$(LI If the result is 0, then iteration should continue.
$(LI If the result is `0`, then iteration should continue.
If there are no more elements to iterate,
apply must return 0.)
the apply function must return `0`.)
)

$(P The result of calling the delegate will be nonzero if the *ForeachStatement*
$(P The result of the `body` delegate will be nonzero if the *ForeachStatement*
body executes a matching $(GLINK BreakStatement), $(GLINK ReturnStatement), or
$(GLINK GotoStatement) whose matching label is outside the *ForeachStatement*.
)

$(P For example, consider a class that is a container for two elements:)
$(P The $(D opApply) and $(D opApplyReverse) member functions can be overloaded.
Selection works similar to overload resolution by
comparing the number of `foreach` variables and number of parameters of `body` and,
if more than one overload remains unique overload,
matching the parameter types of `body` and each $(I ForeachType)
declared in the $(I ForeachStatement) when given.)

$(BEST_PRACTICE Overload apply functions only with delegates differing in number of parameters
to enable type inference for `foreach` variables.
)

$(P For example, consider a class that is a container for some elements:)

$(SPEC_RUNNABLE_EXAMPLE_RUN
--------------
class Foo
{
uint[] array;

int opApply(scope int delegate(ref uint) dg)
int opApply(scope int delegate(ref uint) body)
{
foreach (e; array)
{
int result = dg(e);
int result = body(e);
if (result)
return result;
}
Expand Down Expand Up @@ -811,57 +823,159 @@ $(CONSOLE
73
82
)
$(PANEL
The `scope` storage class on the $(D dg) parameter means that the delegate does
not escape the scope of the $(D opApply) function (an example would be assigning $(D dg) to a
global variable). If it cannot be statically guaranteed that $(D dg) does not escape, a closure may
be allocated for it on the heap instead of the stack.

$(BEST_PRACTICE Annotate delegate parameters to `opApply` functions with `scope` when possible.)

$(PANEL
The `scope` storage class on the `body` parameter means that the delegate does
not escape the scope of the apply function (an example would be assigning`body` to a
global variable). If it cannot be statically guaranteed that `body` does not escape, a closure may
be allocated for it on the heap instead of the stack.
)

$(P $(B Important:) If $(D opApply) catches any exceptions, ensure that those
exceptions did not originate from the delegate passed to $(D opApply). The user would expect
$(P $(B Important:) If apply functions catch any exceptions, ensure that those
exceptions did not originate from the delegate. The user would expect
exceptions thrown from a `foreach` body to both terminate the loop, and propagate outside
the `foreach` body.
)

$(H4 $(LNAME2 template-op-apply, Template `opApply`))

$(P $(D opApply) can also be a templated function,
which will infer the types of parameters based on the $(I ForeachStatement).
For example:)
$(P `opApply` and `opApplyReverse` can also be a function templates,
which can optionally infer the types of parameters based on the $(I ForeachStatement).
)

$(P $(B Note:) An apply function template cannot infer `foreach` variable types.)

$(SPEC_RUNNABLE_EXAMPLE_RUN
--------------
struct S
{
import std.traits : ParameterTypeTuple; // introspection template
import std.stdio;

int opApply(Dg)(scope Dg dg)
if (ParameterTypeTuple!Dg.length == 2) // foreach with 2 parameters
int opApply(Body)(scope Body body)
{
writeln(2);
return 0;
}

int opApply(Dg)(scope Dg dg)
if (ParameterTypeTuple!Dg.length == 3) // foreach with 3 parameters
{
writeln(3);
pragma(msg, Body);
return 0;
}
}

void main()
{
foreach (int a, int b; S()) { } // calls first opApply function
foreach (int a, int b, float c; S()) { } // calls second opApply function
foreach (int a, int b; S()) { } // int delegate(ref int, ref int) pure nothrow @nogc @safe
foreach (bool b, string s; S()) { } // int delegate(ref bool, ref string) pure nothrow @nogc @safe
//foreach (x; S()) { } // Error: cannot infer type for `foreach` variable `x`, perhaps set it explicitly
}
--------------
)

$(H4 $(LNAME2 template-instance-op-apply, `opApply` as an alias to a explicit function template instance))

$(P `opApply` and `opApplyReverse` can be aliases to an appropriate function or function template.
However, special treatment is given to apply functions that are aliases of an appropriate function template instance.)

$(P In that case, the function template instance is used for overload selection and `foreach` variable type inference,
but the template is instantiated again with the actual delegate type of the `foreach` body.
This way, apply functions can infer attributes depending on the attributes of `body` delegate.)

$(SPEC_RUNNABLE_EXAMPLE_RUN
--------------
struct A
{
int opApply(scope int delegate(long) body) => body(42);
}
struct B
{
int opApply(Body)(scope Body body) => body(42);
}
struct C
{
int opApplyImpl(Body)(scope Body body) => body(42);
alias opApply = opApplyImpl!(int delegate(long));
}
void main() @nogc nothrow pure @safe
{
// Error: `@nogc` function `D main` cannot call non-@nogc function `onlineapp.A.opApply`
// Error: `pure` function `D main` cannot call impure function `onlineapp.A.opApply`
// Error: `@safe` function `D main` cannot call `@system` function `onlineapp.A.opApply`
// Error: function `onlineapp.A.opApply` is not `nothrow`
static assert(!__traits(compiles, () @safe {
foreach (x; A()) { }
}));

// Error: cannot infer type for `foreach` variable `x`, perhaps set it explicitly
static assert(!__traits(compiles, {
foreach (x; B()) { }
}));

// Good:
foreach (x; C())
{
static assert(is(typeof(x) == long));
assert(x == 42);
}
}
--------------
)

$(P The `opApplyImpl` pattern is generally preferable to
overloading many apply functions with all possible combinations of attributes.)

$(P Multiple apply function aliases can exist, and selection and `foreach` variable type inference work:)

--------------
class Tree(T)
{
private T label;
private Tree[] children;

this(T label, Tree[] children = null)
{
this.label = label;
this.children = children;
}

alias opApply = opApplyImpl!(int delegate(ref T label));
alias opApply = opApplyImpl!(int delegate(size_t depth, ref T label));
alias opApply = opApplyImpl!(int delegate(size_t depth, bool isLastChild, ref T label));

int opApplyImpl(Body)(scope Body body) => opApplyImpl2(0, true, body);
int opApplyImpl2(Body)(size_t depth, bool lastChild, scope Body body)
{
import std.meta : AliasSeq;
static if (is(Body : int delegate(size_t, bool, ref T)))
alias args = AliasSeq!(depth, lastChild, label);
else static if (is(Body : int delegate(size_t, ref T)))
alias args = AliasSeq!(depth, label);
else
alias args = label;
if (auto result = body(args)) return result;
foreach (i, child; children)
{
if (auto result = child.opApplyImpl2!Body(depth + 1, i + 1 == children.length, body))
return result;
}
return 0;
}
}

void printTree(Tree)(Tree tree)
{
// Selects the unique one with 2 parameters.
// Infers types: size_t and whatever label type the tree has.
foreach (depth, ref label; tree)
{
import std.stdio;
foreach (_; 0 .. depth) write(" ");
writeln(label);
}
}

void main() @safe
{
alias T = Tree!int;
printTree(new T(0, [new T(1, [new T(2), new T(3)]), new T(4, [new T(5)])]));
}
--------------

$(H3 $(LEGACY_LNAME2 foreach_with_ranges, foreach-with-ranges, Foreach over Structs and Classes with Ranges))

$(P If the aggregate expression is a struct or class object, but the
Expand Down Expand Up @@ -1028,16 +1142,16 @@ $(H3 $(LNAME2 foreach_over_delegates, Foreach over Delegates))
$(SPEC_RUNNABLE_EXAMPLE_RUN
--------------
// Custom loop implementation, that iterates over powers of 2 with
// alternating sign. The foreach loop body is passed in dg.
int myLoop(scope int delegate(int) dg)
// alternating sign. The foreach loop body is passed in `body`.
int myLoop(scope int delegate(int) body)
{
for (int z = 1; z < 128; z *= -2)
{
auto ret = dg(z);
auto result = body(z);

// If the loop body contains a break, ret will be non-zero.
if (ret != 0)
return ret;
// If the loop body contains a break, result will be non-zero.
if (result != 0)
return result;
}
return 0;
}
Expand Down Expand Up @@ -1583,7 +1697,7 @@ foreach (item; list)
$(P Any intervening $(RELATIVE_LINK2 try-statement, `finally`) clauses are executed,
and any intervening synchronization objects are released.)

$(P $(D Note:) If a `finally` clause executes a `throw` out of the finally
$(P $(B Note:) If a `finally` clause executes a `throw` out of the finally
clause, the continue target is never reached.)

$(H2 $(LEGACY_LNAME2 BreakStatement, break-statement, Break Statement))
Expand Down