了解 mixin 与 mixin 模板

Understanding mixins vs mixin templates

在学习D语言的过程中,我正在尝试做一个通用的Matrixclass,它支持包含对象的类型提升。

也就是说,当我将 Matrix!(int) 乘以 Matrix!(real) 时,结果应该是 Matrix!(real)

因为 type promotions 有很多不同的种类,为每一种可能的组合重新实现 opBinary 方法真的很乏味,而且会产生大量的样板代码。所以 mixins/mixin 模板似乎就是答案。

我不明白的是为什么第一个代码示例有效

import std.stdio;
import std.string : format;


string define_opbinary(string other_type) {
    return "
        Matrix opBinary(string op)(Matrix!(%s) other) {
            if(op == \"*\") {
                Matrix result;
                if(this.columns == other.rows) {
                    result = new Matrix(this.rows, other.columns);
                } else {
                    result = new Matrix(0,0);
                }
                return result;
            } else assert(0, \"Operator \"~op~\" not implemented\");
        }
        ".format(other_type);
}


class Matrix(T) {
    T[][] storage;
    size_t rows;
    size_t columns;
    const string type = T.stringof;

    this(size_t rows, size_t columns) {
        this.storage = new T[][](rows, columns);
        this.rows = rows;
        this.columns = columns;
    }

    void opIndexAssign(T value, size_t row, size_t column) {
        storage[row][column] = value;
    }


    mixin(define_opbinary(int.stringof));
    mixin(define_opbinary(uint.stringof));
}



void main()
{
    Matrix!int mymat = new Matrix!(int)(2, 2);
    mymat[0,0] = 5;
    writeln(mymat.type);

    Matrix!uint mymat2 = new Matrix!(uint)(2, 2);
    writeln(mymat2.type);
    auto result = mymat * mymat2;
    writeln("result.rows=", result.rows);
    writeln("result.columns=", result.columns);
    auto result2 = mymat2 * mymat;
    writeln("result.type=",result.type);
    writeln("result2.type=",result2.type);
}

配音输出:

Performing "debug" build using /usr/bin/dmd for x86_64.
matrix ~master: building configuration "application"...
Linking...
Running ./matrix.exe 
50
00
int
uint
result.rows=2
result.columns=2
00
00
result.type=int
result2.type=uint

但是第二个代码示例不起作用

import std.stdio;
import std.string : format;


mixin template define_opbinary(alias other_type) {
    Matrix opBinary(string op)(Matrix!(other_type) other) {
        if(op == "*") {
            Matrix result;
            if(this.columns == other.rows) {
                result = new Matrix(this.rows, other.columns);
            } else {
                result = new Matrix(0,0);
            }
            return result;
        } else assert(0, "Operator "~op~" not implemented");
    }
}


class Matrix(T) {
    T[][] storage;
    size_t rows;
    size_t columns;
    const string type = T.stringof;

    this(size_t rows, size_t columns) {
        this.storage = new T[][](rows, columns);
        this.rows = rows;
        this.columns = columns;
    }

    void opIndexAssign(T value, size_t row, size_t column) {
        storage[row][column] = value;
    }

    mixin define_opbinary!(int);
    mixin define_opbinary!(uint);
}



void main()
{
    Matrix!int mymat = new Matrix!(int)(2, 2);
    mymat[0,0] = 5;
    writeln(mymat.type);

    Matrix!uint mymat2 = new Matrix!(uint)(2, 2);
    writeln(mymat2.type);
    auto result = mymat * mymat2;
    writeln("result.rows=", result.rows);
    writeln("result.columns=", result.columns);
    auto result2 = mymat2 * mymat;
    writeln("result.type=",result.type);
    writeln("result2.type=",result2.type);
}

配音输出:

source/app.d(60,19): Error: cast(Object)mymat is not of arithmetic type, it is a object.Object
source/app.d(60,27): Error: cast(Object)mymat2 is not of arithmetic type, it is a object.Object
source/app.d(64,20): Error: cast(Object)mymat2 is not of arithmetic type, it is a object.Object
source/app.d(64,29): Error: cast(Object)mymat is not of arithmetic type, it is a object.Object
/usr/bin/dmd failed with exit code 1.

非常奇怪的是,如果我删除 mixin define_opbinary!(int); 调用,那么我只会收到两个算术投诉(只剩下关于第 60 行 (auto result = mymat * mymat2;) 的两个投诉)。

我感觉编译器以某种方式将这两个 mixin 调用视为模棱两可并删除了它们,但我不确定。

如有任何帮助,我们将不胜感激。

哦,关于这个我有很多话要说,包括我不会为此使用任何一种类型的 mixin - 我只会使用普通模板。我会在最后回到那个。

我会尽量做到相当全面,所以如果我描述了你已经知道的东西,我深表歉意,另一方面,我可能也会给出一些无关紧要的东西 material 为了提供综合背景 material 加深理解。

首先,mixin 与模板 mixin。 mixin() 接受一个字符串,将其解析为一个 AST 节点(顺便说一句,AST 是编译器用于表示代码的内部数据结构,它代表 "abstract syntax tree"。foo() 是一个 AST 节点,例如 FunctionCall { args: [] }if(foo) {} 类似于 IfStatement { condition: Expression { arg: Variable { name: foo }, body : EmptyStatement } - 基本上 object 代表代码的每一部分)。

然后它将解析的 AST 节点粘贴到出现 mixin 单词的同一位置。你通常可以将其视为 copy/pasting 代码字符串,但这里的限制是字符串必须代表一个完整的元素,并且它必须在 mixin 没有错误的相同上下文中是可替换的。所以就像你不能做 int a = bmixin(c) 来创建一个前面有 b 的变量——mixin 必须自己代表一个完整的节点。

不过,一旦它粘贴到该 AST 节点中,编译器就会将其视为代码最初全部写在那里。将在粘贴的上下文等中查找引用的任何名称。

另一方面,模板混合实际上在 AST 中仍然有一个容器元素,用于名称查找。它实际上类似于编译器内部的 structclass - 它们都有一个 child 声明列表,这些声明作为一个单元保持在一起。

最大的区别是模板混合的内容可以从 parent 上下文中自动访问...通常。它遵循类似于 class 继承的规则,其中 class Foo : Bar 可以看到 Bar 的成员,就好像它们是它自己的成员一样,但它们仍然保持独立。您仍然可以像 super.method(); 一样,独立于 child 的覆盖来调用它。

"usually" 的出现是因为重载和劫持规则。深入研究和基本原理:https://dlang.org/articles/hijack.html

但它的缺点是为了防止第三方代码在添加新函数时悄悄地改变您的程序的行为,D 要求在使用点合并所有函数重载集程序员,它对运算符重载特别挑剔,因为它们已经具有任何 mixin 都将要修改的默认行为。

mixin template B(T) {
   void foo(T t) {}
}
class A {
   mixin B!int;
   mixin B!string;
}

这与您的代码类似,但具有普通功能。如果你编译并 运行,它会工作。现在,让我们直接向 A:

添加一个 foo 重载
mixin template B(T) {
   void foo(T t) {}
}
class A {
   mixin B!int;
   mixin B!string;

   void foo(float t) {}
}

如果您尝试使用字符串参数编译它,它实际上会失败! "Error: function poi.A.foo(float t) is not callable using argument types (string)"。为什么它不使用 mixin?

这是模板混入的规则 - 请记住,编译器仍然将它们视为一个单元,而不仅仅是一组粘贴的声明。外部 object 上出现的任何名称 - 在这里,我们的 class A - 将被使用而不是在模板 mixin 内部查找。

因此,它会看到 A.foo 而不会费心查看 B 来查找 foo。这对于从模板混合中覆盖特定的东西很有用,但在尝试添加重载时可能会很麻烦。解决办法是在top-level中添加一行alias,告诉编译器专门往里面找。首先,我们需要给 mixin 一个名字,然后显式转发这个名字:

mixin template B(T) {
   void foo(T t) {}
}
class A {
   mixin B!int bint; // added a name here
   mixin B!string bstring; // and here

   alias foo = bint.foo; // forward foo to the template mixin
   alias foo = bstring.foo; // and this one too

   void foo(float t) {}
}

void main() {
    A a = new A;
    a.foo("a");
}

现在它适用于浮点数、整数和字符串....但它也有点违背了模板混入添加重载的目的。你可以使用的一个技巧是在 A 中放置一个 top-level 模板函数,它只是转发给 mixins...只是他们需要一个不同的名称来注册。

这让我想起了你的代码。就像我说的,D 对运算符重载特别挑剔,因为它们总是覆盖正常行为(即使正常行为是错误,如 classes)。您需要在顶层明确说明它们。

考虑以下几点:

import std.stdio;
import std.string : format;


mixin template define_opbinary(alias other_type) {
    // I renamed this to opBinaryHelper since it will not be used directly
    // but rather called from the top level
    Matrix opBinaryHelper(string op)(Matrix!(other_type) other) {
        if(op == "*") {
            Matrix result;
            if(this.columns == other.rows) {
                result = new Matrix(this.rows, other.columns);
            } else {
                result = new Matrix(0,0);
            }
            return result;
        } else assert(0, "Operator "~op~" not implemented");
    }
}


class Matrix(T) {
    T[][] storage;
    size_t rows;
    size_t columns;
    const string type = T.stringof;

    this(size_t rows, size_t columns) {
        this.storage = new T[][](rows, columns);
        this.rows = rows;
        this.columns = columns;
    }

    void opIndexAssign(T value, size_t row, size_t column) {
        storage[row][column] = value;
    }

    mixin define_opbinary!(int);
    mixin define_opbinary!(uint);

    // and now here, we do a top-level opBinary that calls the helper
    auto opBinary(string op, M)(M rhs) {
        return this.opBinaryHelper!(op)(rhs);
    }
}



void main()
{
    Matrix!int mymat = new Matrix!(int)(2, 2);
    mymat[0,0] = 5;
    writeln(mymat.type);

    Matrix!uint mymat2 = new Matrix!(uint)(2, 2);
    writeln(mymat2.type);
    auto result = mymat * mymat2;
    writeln("result.rows=", result.rows);
    writeln("result.columns=", result.columns);
    auto result2 = mymat2 * mymat;
    writeln("result.type=",result.type);
    writeln("result2.type=",result2.type);
}

我粘贴了完整的代码,但实际上只有两个变化:mixin 模板现在定义了一个具有不同名称的助手 (opBinaryHelper),以及 top-level class 现在有一个明确的 opBinary 定义转发给所述助手。 (顺便说一下,如果你要添加其他重载,上面的 alias 技巧可能是必要的,但在这种情况下,因为它是从一个名称内部在 if 上全部调度的,它可以让你合并所有助手自动。)

代码终于可以运行了。

现在,为什么字符串混合不需要这些?好吧,回到最初的定义:一个字符串 mixin 解析它,然后粘贴到 AST 节点中/就好像它最初是在那里写的一样/。后一部分让它起作用(只是以一旦你混合了一个字符串为代价,你就被它困住了,所以如果你不喜欢它的一部分,你必须修改 te 库而不是仅仅覆盖一部分)。

模板 mixin 维护自己的 sub-namespace 以允许选择性覆盖等,这会触发这些更严格的重载规则的犯规。


最后,这是我的实际做法:

// this MatrixType : stuff magic means to accept any Matrix, and extract
// the other type out of it.
// a little docs: https://dlang.org/spec/template.html#alias_parameter_specialization

// basically, write a pattern that represents the type, then comma-separate
// a list of placeholders you declared in that pattern

auto opBinary(string op, MatrixType : Matrix!Other_Type, Other_Type)(MatrixType other) {
    // let the compiler do the promotion work for us!
    // we just fetch the type of regular multiplication between the two types
    // the .init just uses the initial default value of the types as a placeholder,
    // all we really care about is the type, just can't multiply types, only
    // values hence using that.
    alias PromotedType = typeof(T.init * Other_Type.init);

    // in your version, you used `if`, but since this is a compile-time
    // parameter, we can use `static if` instead and get more flexibility
    // on stuff like actually changing the return value per operation.
    //
    // Don't need it here, but wanted to point it out anyway.
    static if(op == "*") {
        // and now use that type for the result
        Matrix!PromotedType result;
        if(this.columns == other.rows) {
            result = new Matrix!PromotedType(this.rows, other.columns);
        } else {
            result = new Matrix!PromotedType(0,0);
        }
        return result;
    // and with static if, we can static assert to turn that runtime
    // exception into a compile-time error
    } else static assert(0, "Operator "~op~" not implemented");
}

只需将 opBinary 放入您的 class 中,现在一个函数就可以处理所有情况 - 无需列出特定类型,因此根本不需要 mixin 魔法! (....好吧,除非你需要用 child classes 进行虚拟覆盖,但那是另一个话题。简短的提示,可以 static foreach 我谈到过在我最后的回答中: )

那个小函数里有一些D技巧,但我试着在代码的注释中解释。随意询问您是否需要更多说明 - 模板中的那些 : patterns 是 IMO 中更高级的 D compile-time 反射事物之一,因此一开始它们并不容易获得,但对于简单的情况像这样,还挺有道理的,就当是一个带占位符的声明吧。