Thread overview
opApply seems like it can infer delegate types AND parameters!?
December 11

In an attempt to come up with an answer to a post, I found something odd, but useful, that I nonetheless don’t understand.

As far as I know – or rather thought I knew – foreach loops can infer the types of the “loop variables” if an opApply is a function, not a function template, and the number and ref-ness of loop variables, as well as the function attributes, disambiguate the overload.

What seems to be possible, and always was since version 2.060, is actually combining the two:

  1. Instead of implementing a function opApply(scope int delegate(...)), write a function template opApplyImpl(DG)(scope int delegate(...)) (or whatever name) and let it take the delegate type as a template type parameter.
  2. Make opApply an alias to an instance of the template, passing the desired delegate type as an argument.

You can even do multiple templates or alias different instances to the same template.

I always thought you had to provide aliases with all 16 combinations of the attributes @safe, @nogc, pure, and nothrow for each actually desired instance. But you don’t and I have no clue why.

Why does it work?

Because the Shorten on run.dlang.io doesn’t seem to work, here’s the full code:

struct WithIndexType(T, U)
{
    U[] array;

    int opApplyImpl(DG)(scope DG callback)
    {
        pragma(msg, "opApplyImpl(", DG, ")");
        for (T index = 0; index < cast(T)array.length; ++index)
        {
            import std.traits : Parameters;
            static if (Parameters!DG.length == 1)
            {
                if (auto result = callback(array[index])) return result;
            }
            else static if (Parameters!DG.length == 2)
            {
                if (auto result = callback(index, array[index])) return result;
            }
            else
            {
                static assert(0, "DG is not a callable type");
            }
        }
        return 0;
    }
    alias opApply = opApplyImpl!(int delegate(T, ref U));
    alias opApply = opApplyImpl!(int delegate(ref U));
}

auto withIndexType(T, U)(return scope U[] values) @safe pure nothrow @nogc
{
    return WithIndexType!(T, U)(values);
}

void main() @safe
{
    import std.stdio;

    double[] xs = new double[](20);
    foreach (i, ref d; xs.withIndexType!byte)
    {
        static assert(is(typeof(i) == byte));
        static assert(is(typeof(d) == double));
        d = i + 1;
    }
    foreach (d; xs.withIndexType!byte)
    {
        static assert(is(typeof(d) == double));
        write(d, ' ');
    }
}

The pragma shows that the template is being instantiated with the actual types (in terms of attributes and ref-ness) of the generated closure.

opApplyImpl(int delegate(byte, ref double))
opApplyImpl(int delegate(ref double))
opApplyImpl(int delegate(byte, ref double) pure nothrow @nogc @safe)
opApplyImpl(int delegate(ref double) @safe)

If you don’t use a template and aliased instance, i.e. you just use the following, you get errors because of attributes:

    int opApply(scope int delegate(T, ref U) callback)
    {
    	for (T index = 0; index < cast(T)array.length; ++index)
        {
        	if (auto result = callback(index, array[index])) return result;
        }
        return 0;
    }

    int opApply(scope int delegate(ref U) callback)
    {
        for (T index = 0; index < cast(T)array.length; ++index)
        {
            if (auto result = callback(array[index])) return result;
        }
        return 0;
    }
Error: `@safe` function `D main` cannot call `@system` function `onlineapp.WithIndexType!(byte, double).WithIndexType.opApply`
        which wasn't inferred `@safe` because of:
       `@safe` function `opApply` cannot call `@system` `callback`
       `onlineapp.WithIndexType!(byte, double).WithIndexType.opApply` is declared here

(The error appears twice as main contains two loops.)
I would have expected this error regardless whether the called opApply is a function or an aliased function template instance, but apparently, it makes a difference.

The fact that the enclosing struct is a template doesn’t affect it either.

December 11

On Monday, 11 December 2023 at 23:21:45 UTC, Quirin Schroll wrote:

>

[…]

  1. Instead of implementing a function opApply(scope int delegate(...)), write a function template opApplyImpl(DG)(scope int delegate(...)) (or whatever name) and let it take the delegate type as a template type parameter.

[Correction] This should have been:

  1. Instead of implementing a function opApply(scope int delegate(...)), write a function template opApplyImpl(DG)(scope DG) (or whatever name) and let it take the delegate type as a template type parameter.
December 12

On Monday, 11 December 2023 at 23:21:45 UTC, Quirin Schroll wrote:

>

I always thought you had to provide aliases with all 16 combinations of the attributes @safe, @nogc, pure, and nothrow for each actually desired instance. But you don’t and I have no clue why.

Why does it work?

Truly bizarre. I can't think of any explanation other than a compiler bug. Probably not a good idea to rely on this, even if the result is pretty handy.