当前位置: 首页 > 科技观察

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

时间:2023-03-16 21:49:02 科技观察

代码生成器是非常有用的工具。我有时会使用jinja2的命令行版本来生成高度冗余的配置文件和其他文本文件,但它在转换数据方面的能力有限。显然Jinja2作者有不同的想法,我想要类似列表理解或D的可组合范围算法的东西。我决定制作一个类似于Jinja2的工具,但让我通过使用范围算法转换数据来生成复杂的文件。这个想法很简单:一种直接用D代码重写的模板语言。因为是D语言,所以D语言能做的一切它都能支持。我想要一个独立的代码生成器,但由于D语言的混入特性,相同的模板语言可以用作嵌入式模板语言(例如Web应用程序中的HTML)。有关此技巧的更多信息,请参阅这篇关于在编译时使用mixins将Brainfuck转换为D和机器代码的文章。和往常一样,源代码在GitLab上。本文中的示例也可以在这里找到。Helloworld示例下面是一个演示该想法的示例:Hello[=retro("dlrow")]![:枚举一=1;:]1+1=[=one+one][=some_expression]类似于Jinja2中的{{some_expression}},在输出中渲染一个值。[:一些声明;:]类似于{%some_statement%},用于执行完整的代码语句。我更改了语法,因为D也大量使用花括号,混合使用这两者会使模板难以阅读(有一些特殊的非D指令,例如include,它们被包裹在[<和>])。如果将以上内容保存到名为hello.txt.dj的文件中并运行djinn命令行工具,您将得到一个名为hello.txt的文件,其中包含您可能猜到的内容:Helloworld!1+1=2如果您使用过Jinja2,您可能想知道第二行发生了什么。Djinn有一个简化格式化和空格处理的特殊规则:如果源代码行包含[:语句或[<指令但不包含任何非空格输出,则忽略整行输出。空行仍按原样呈现。生成数据好的,现在来做一些更实际的事情:生成CSV数据。x,f(x)[:导入std.mathspecial;foreach(x;iota(-1.0,1.0,0.1)):][="%0.1f,%g",x,normalDistribution(x)]A[=和]对可以包含多个用逗号分隔的表达式。如果第一个表达式是用双引号括起来的字符串,它将被解释为格式字符串。这是输出: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.424070-0.2,0.42070,0.50.1,0.5398280.2,0.579260.3,0.6179110.4,0.6554220.5,0.6914620.6,0.7257470.7,0.7580360.2,0.579260.8,0.788145Image0.9,0.4TheclassicNetpbmimagelibrarydefinesabunchofimageformats,someofwhicharetext-基于。比如这是一张3x3的矢量图:P2#PGM格式标识33#宽高7#代表纯白的数值(0代表黑色)707000707你可以把上面的文字保存到一个名为cross.pgm的文件,许多图像工具都知道如何解析它。下面是一些以相同格式生成Mandelbrot集分形的Djinn代码:[:importstd.complex;枚举W=640;枚举H=480;枚举kMaxIter=20;ubytemb(uintx,uinty){constc=complex(3.0*(x-W/1.5)/W,2.0*(y-H/2.0)/H);汽车z=复杂(0.0);ubyteret=kMaxIter;while(abs(z)<=2&&--ret)z=z*z+c;返还;}:]P2[=W][=H][=kMaxIter][:foreach(y;0..H):][="%(%s%)",iota(W).map!(x=>mb(x,y))]生成的文件大约为800kB,但它很好地压缩为PNG:$#使用GraphicsMagick转换$gmconvertmandelbrot.pgmmandelbrot.png结果如下:SolvethepuzzleHereis一个谜题:一个5行5列的格子,需要用1到5的数字填满,每个数字在每一行限制使用一次,并且每列只能使用一次(即做一个5行的拉丁方和5列)。相邻单元格中的数字也必须满足>大于号表示的所有不等式。几个月前我用过线性规划(LP)。线性规划问题是具有线性约束的连续变量系统。这次我将使用混合整数线性规划(MILP),它通过允许整数约束变量来概括LP。事实证明,这足以成为NP完全问题,而MILP恰好很好地模拟了这个难题。在之前的一篇文章中,我使用了Julia库JuMP来帮助解决这个问题。这次我将使用CPLEX:基于文本的格式,它受多个LP和MILP求解器的支持(如果需要,可以使用现成的工具轻松转换为其他格式)。这是上一篇文章中CPLEX格式的LP:最小化对象:v主题Topptotal:pr+pp+ps=1rock:4ps-5pp-v<=0paper:5pr-8ps-v<=0scissors:8pp-4pr-v<=0Bounds0<=pr<=10<=pp<=10<=ps<=1EndCPLEX格式易读,但复杂问题需要大量变量和约束,这使得手工编码变得痛苦且容易出错。有特定领域的语言,例如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约束:\cellhasonlyonevalue[: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代码看起来像这样:s+%)",vs.map!(v=>text(v,'',ivar(r,c,v)))]-[=vvar(r,c)]=0[::]相邻约束左下角的不等符号和值为4的单元格很容易写。剩下的就是将指示变量声明为二进制并为变量组v设置边界。连同变量边界,总共有150个变量和111个约束。您可以在存储库中查看完整代码。GNU线性规划工具集有一个命令行工具可以解决这个CPLEXMILP。不幸的是,输出是一个包含所有内容的巨大转储,因此我使用awk命令提取我需要的内容:$timeglpsol--lpinequality.lp-o/dev/stdout|awk'/v[0-9][0-9]/{打印$2,$4}'|排序v001v013v022v035v044v102v115v124v131v143v203v211v225v234v242v305v314v323v332v341v404v412v421v433v445real0m0.114suser0m0.106ssys0m0.005s这是在原始网格中写出的解决方案:这些例子只是为了好玩,但我相信你明白了。顺便说一句,Djinn存储库本身的README.md文件是使用Djinn模板生成的。正如我所说,Djinn也可以用作嵌入到D代码中的编译时模板语言。我最初只想要一个代码生成器,这要归功于D的元编程功能。