Djinn:一個受 Jinja2 啟發的代碼生成器和模板語言
代碼生成器是非常有用的工具。我有時使用 jinja2 的命令行版本來生成高度冗餘的配置文件和其他文本文件,但它在轉換數據方面功能有限。顯然,Jinja2 的作者有不同的想法,而我想要類似於 列表推導 或 D 語言的 可組合範圍 演算法之類的東西。
我決定製作一個類似於 Jinja2 的工具,但讓我可以通過使用範圍演算法轉換數據來生成複雜的文件。這個想法非常簡單:一個直接用 D 語言代碼重寫的模板語言。因為它 就是 D 語言,它可以支持 D 語言所能做的一切。我想要一個獨立的代碼生成器,但是由於 D 語言的 mixin
特性,同樣的模板語言可以作為嵌入式模板語言工作(例如,Web 應用程序中的 HTML)。有關該技巧的更多信息,請參閱 這篇 關於在編譯時使用 mixins 將 Brainfuck 轉換為 D 和機器代碼的文章。
像往常一樣,源碼在 GitLab 上。這篇文章中的例子也可以在這裡找到。
Hello world 示例
這是一個演示這個想法的例子:
Hello [= retro("dlrow") ]!
[: enum one = 1; :]
1 + 1 = [= one + one ]
[= some_expression ]
類似於 Jinja2 中的 {{ some_expression }}
,它在輸出中呈現一個值。[: some_statement; :]
類似於 {% some_statement %}
,用於執行完整的代碼語句。我更改了語法,因為 D 也大量使用花括弧,並且將兩者混合使模板難以閱讀(還有一些特殊的非 D 指令,比如 include
,它們被包裹在 [<
和 >]
中)。
如果你將上面的內容保存到一個名為 hello.txt.dj
的文件中並運行 djinn
命令行工具,你會得到一個名為 hello.txt
的文件,其中包含你可能猜到的內容:
Hello world!
1 + 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) ]
一個 [=
和 ]
對可以包含多個用逗號分隔的表達式。如果第一個表達式是一個由雙引號包裹的字元串,則會被解釋為 格式化字元串。下面是輸出結果:
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
製作圖片
這個例子展示了一個圖片的生成過程。經典的 Netpbm 圖像庫定義了一堆圖像格式,其中一些是基於文本的。例如,這是一個 3 x 3 向量的圖像:
P2 # PGM 格式標識
3 3 # 寬和高
7 # 代表純白色的值(0 代表黑色)
7 0 7
0 0 0
7 0 7
你可以將上述文本保存到名為 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)) ]
生成的文件大約為 800 kB,但它可以很好地被壓縮為 PNG:
$ # 使用 GraphicsMagick 進行轉換
$ gm convert mandelbrot.pgm mandelbrot.png
結果如下:
解決謎題
這裡有一個謎題:
一個 5 行 5 列的網格需要用 1 到 5 的數字填充,每個數字在每一行中限使用一次,在每列中限使用一次(即,製作一個 5 行 5 列的 拉丁方格 )。相鄰單元格中的數字還必須滿足所有 >
大於號表示的不等式。
幾個月前我使用了 線性規劃 (LP)。線性規劃問題是具有線性約束的連續變數系統。這次我將使用 混合整數線性規劃 (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
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]
可以使用以下 Djinn 代碼生成對所有行和列的 CPLEX 約束:
單元格只有一個值
[:
foreach (r; iota(N))
foreach (c; iota(N))
:]
[= "%-(%s + %)", vs.map!(v => ivar(r, c, v)) ] = 1
[::]
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}]
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
[::]
不等符號相鄰的和左下角值為為 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
這是在原始網格中寫出的解決方案:
這些例子只是用來玩的,但我相信你已經明白了。順便說一下,Djinn 代碼倉庫的 README.md
文件本身是使用 Djinn 模板生成的。
正如我所說,Djinn 也可以用作嵌入在 D 語言代碼中的編譯期模板語言。我最初只是想要一個代碼生成器,得益於 D 語言的元編程功能,這算是一個額外獲得的功能。
via: https://theartofmachinery.com/2021/01/01/djinn.html
作者:Simon Arneaud 選題:lujun9972 譯者:hanszhao80 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive