深圳幻海软件技术有限公司 欢迎您!

Djinn:一个受 Jinja2 启发的代码生成器和模板语言

2023-02-28

代码生成器是非常有用的工具。我有时使用 jinja2 的命令行版本来生成高度冗余的配置文件和其他文本文件,但它在转换数据方面功能有限。显然,Jinja2的作者有不同的想法,而我想要类似于 列表推导listcomprehensions 或D语言的 可组

代码生成器是非常有用的工具。我有时使用 jinja2 的命令行版本来生成高度冗余的配置文件和其他文本文件,但它在转换数据方面功能有限。显然,Jinja2 的作者有不同的想法,而我想要类似于 列表推导list comprehensions 或 D 语言的 可组合范围composable range 算法之类的东西。

我决定制作一个类似于 Jinja2 的工具,但让我可以通过使用范围算法转换数据来生成复杂的文件。这个想法非常简单:一个直接用 D 语言代码重写的模板语言。因为它 就是 D 语言,它可以支持 D 语言所能做的一切。我想要一个独立的代码生成器,但是由于 D 语言的 mixin 特性,同样的模板语言可以作为嵌入式模板语言工作(例如,Web 应用程序中的 HTML)。有关该技巧的更多信息,请参阅 ​​这篇​​ 关于在编译时使用 mixins 将 Brainfuck 转换为 D 和机器代码的文章。

像往常一样,​​源码在 GitLab 上​​​。​​这篇文章中的例子也可以在这里找到​​。

Hello world 示例

这是一个演示这个想法的例子:

Hello [= retro("dlrow") ]!
    [: enum one = 1; :]
    1 + 1 = [= one + one ]
  • 1.
  • 2.
  • 3.

​[= some_expression ]​​​ 类似于 Jinja2 中的 ​​{{ some_expression }}​​​,它在输出中呈现一个值。​​[: some_statement; :]​​​ 类似于 ​​{% some_statement %}​​​ ,用于执行完整的代码语句。我更改了语法,因为 D 也大量使用花括号,并且将两者混合使模板难以阅读(还有一些特殊的非 D 指令,比如 ​​include​​​,它们被包裹在 ​​[<​​​ 和 ​​>]​​ 中)。

如果你将上面的内容保存到一个名为 ​​hello.txt.dj​​​ 的文件中并运行 ​​djinn​​​ 命令行工具,你会得到一个名为 ​​hello.txt​​ 的文件,其中包含你可能猜到的内容:

Hello world!
    1 + 1 = 2
  • 1.
  • 2.

如果你使用过 Jinja2,你可能想知道第二行发生了什么。Djinn 有一个简化格式化和空格处理的特殊规则:如果源代码行包含 ​​[:​​​ 语句或 ​​[<​​ 指令但不包含任何非空格输出,则整行都会被忽略输出。空行则仍会原样呈现。

生成数据

好的,现在来讲一些更实用的东西:生成 CSV 数据。

x,f(x)
    [: import std.mathspecial;
    foreach (x; iota(-1.0, 1.0, 0.1)) :]
    [= "%0.1f,%g", x, normalDistribution(x) ]
  • 1.
  • 2.
  • 3.
  • 4.

一个 ​​[=​​​ 和 ​​]​​​ 对可以包含多个用逗号分隔的表达式。如果第一个表达式是一个由双引号包裹的字符串,则会被解释为 ​​格式化字符串​​。下面是输出结果:

x,f(x)
    -1.0,0.158655
    -0.9,0.18406
    -0.8,0.211855
    -0.7,0.241964
    -0.6,0.274253
    -0.5,0.308538
    -0.4,0.344578
    -0.3,0.382089
    -0.2,0.42074
    -0.1,0.460172
    0.0,0.5
    0.1,0.539828
    0.2,0.57926
    0.3,0.617911
    0.4,0.655422
    0.5,0.691462
    0.6,0.725747
    0.7,0.758036
    0.8,0.788145
    0.9,0.81594
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

制作图片

这个例子展示了一个图片的生成过程。​​经典的 Netpbm 图像库定义了一堆图像格式​​,其中一些是基于文本的。例如,这是一个 3 x 3 向量的图像:

P2 # PGM 格式标识
    3 3 # 宽和高
    7 # 代表纯白色的值(0 代表黑色)
    7 0 7
    0 0 0
    7 0 7
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

你可以将上述文本保存到名为 ​​cross.pgm​​​ 之类的文件中,很多图像工具都知道如何解析它。下面是一些 Djinn 代码,它以相同的格式生成 ​​Mandelbrot 集​​ 分形:

[:
    import std.complex;
    enum W = 640;
    enum H = 480;
    enum kMaxIter = 20;
    ubyte mb(uint x, uint y)
    {
        const c = complex(3.0 * (x - W / 1.5) / W, 2.0 * (y - H / 2.0) / H);
        auto z = complex(0.0);
        ubyte ret = kMaxIter;
        while (abs(z) <= 2 && --ret) z = z * z + c;
        return ret;
    }
    :]
    P2
    [= W ] [= H ]
    [= kMaxIter ]
    [: foreach (y; 0..H) :]
    [= "%(%s %)", iota(W).map!(x => mb(x, y)) ]
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

生成的文件大约为 800 kB,但它可以很好地被压缩为 PNG:

$ # 使用 GraphicsMagick 进行转换
    $ gm convert mandelbrot.pgm mandelbrot.png
  • 1.
  • 2.

结果如下:

解决谜题

这里有一个谜题:

一个 5 行 5 列的网格需要用 1 到 5 的数字填充,每个数字在每一行中限使用一次,在每列中限使用一次(即,制作一个 5 行 5 列的拉丁方格Latin square)。相邻单元格中的数字还必须满足所有 ​​>​​ 大于号表示的不等式。

​​几个月前我使用了 线性规划linear programming(LP)​​。线性规划问题是具有线性约束的连续变量系统。这次我将使用混合整数线性规划mixed integer linear programming(MILP),它通过允许整数约束变量来归纳 LP。事实证明,这足以成为 NP 完备的,而 MILP 恰好可以很好地模拟这个谜题。

在上一篇文章中,我使用 Julia 库 JuMP 来帮助解决这个问题。这次我将使用 ​​CPLEX:基于文本的格式​​,它受到多个 LP 和 MILP 求解器的支持(如果需要,可以通过现成的工具轻松转换为其他格式)。这是上一篇文章中 CPLEX 格式的 LP:

Minimize
      obj: v
    Subject To
      ptotal: pr + pp + ps = 1
      rock: 4 ps - 5 pp - v <= 0
      paper: 5 pr - 8 ps - v <= 0
      scissors: 8 pp - 4 pr - v <= 0
    Bounds
      0 <= pr <= 1
      0 <= pp <= 1
      0 <= ps <= 1
    End
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

CPLEX 格式易于阅读,但复杂度高的问题需要大量变量和约束来建模,这使得手工编码既痛苦又容易出错。有一些特定领域的语言,例如 ​​ZIMPL​​,用于以高级方式描述 MILP 和 LP。对于许多问题来说,它们非常酷,但最终它们不如具有良好库(如 JuMP)支持的通用语言或使用 D 语言的代码生成器那样富有表现力。

我将使用两组变量来模拟这个谜题:​​v_{r,c}​​​ 和 ​​i_{r,c,v}​​​。​​v_{r,c}​​​ 将保存 r 行 c 列单元格的值(从 1 到 5)。​​i_{r,c,v}​​ 是一个二进制指示器,如果 r 行 c 列的单元格的值是 v,则该指示器值为 1,否则为0。这两组变量是网格的冗余表示,但第一种表示更容易对不等式约束进行建模,而第二种表示更容易对唯一性约束进行建模。我只需要添加一些额外的约束来强制这两个表示是一致的。但首先,让我们从每个单元格必须只有一个值的基本约束开始。从数学上讲,这意味着给定行和列的所有指示器都必须为

0,但只有一个值为 1 的例外。这可以通过以下等式强制约束:

[i_{r,c,1} + i_{r,c,2} + i_{r,c,3} + i_{r,c,4} + i_{r,c,5} = 1]
  • 1.

可以使用以下 Djinn 代码生成对所有行和列的 CPLEX 约束:

\ 单元格只有一个值
    [:
    foreach (r; iota(N))
    foreach (c; iota(N))
    :]
        [= "%-(%s + %)", vs.map!(v => ivar(r, c, v)) ] = 1
    [::]
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

​ivar()​​​ 是一个辅助函数,它为我们提供变量名为 ​​i​​​ 的字符串标识符,而 ​​vs​​​ 存储从 1 到 5 的数字以方便使用。行和列内唯一性的约束完全相同,但在 ​​i​​ 的其他两个维度上迭代。

为了使变量组 ​​i​​​ 与变量组 ​​v​​​ 保持一致,我们需要如下约束(请记住,变量组 ​​i​​ 中只有一个元素的值是非零的):

[i_{r,c,1} + 2i_{r,c,2} + 3i_{r,c,3} + 4i_{r,c,4} + 5i_{r,c,5} = v_{r,c}]
  • 1.

CPLEX 要求所有变量都位于左侧,因此 Djinn 代码如下所示:

\ 连接变量组 i 和变量组 v
    [:
    foreach (r; iota(N))
    foreach (c; iota(N))
    :]
        [= "%-(%s + %)", vs.map!(v => text(v, ' ', ivar(r, c, v))) ] - [= vvar(r,c) ] = 0
    [::]
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

不等符号相邻的和左下角值为为 4 单元格的约束写起来都很简单。剩下的便是将指示器变量声明为二进制,并为变量组 ​​v​​​ 设置边界。加上变量的边界,总共有 150 个变量和 111 个约束 ​​你可以在仓库中看到完整的代码​​。

GNU 线性规划工具集 有一个命令行工具可以解决这个 CPLEX MILP。不幸的是,它的输出是一个包含了所有内容的体积很大的转储,所以我使用 awk 命令来提取需要的内容:

$ time glpsol --lp inequality.lp -o /dev/stdout | awk '/v[0-9][0-9]/ { print $2, $4 }' | sort
    v00 1
    v01 3
    v02 2
    v03 5
    v04 4
    v10 2
    v11 5
    v12 4
    v13 1
    v14 3
    v20 3
    v21 1
    v22 5
    v23 4
    v24 2
    v30 5
    v31 4
    v32 3
    v33 2
    v34 1
    v40 4
    v41 2
    v42 1
    v43 3
    v44 5
    real    0m0.114s
    user    0m0.106s
    sys     0m0.005s
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.

这是在原始网格中写出的解决方案:

这些例子只是用来玩的,但我相信你已经明白了。顺便说一下,Djinn 代码仓库的 ​​README.md​​ 文件本身是使用 Djinn 模板生成的。

正如我所说,Djinn 也可以用作嵌入在 D 语言代码中的编译期模板语言。我最初只是想要一个代码生成器,得益于 D 语言的元编程功能,这算是一个额外获得的功能。