糖果实验室杂货铺

Candy Lab

Writing a DSL in Lua

1 year ago 0

()运操符调用, 链是从一个左值表达式函数(或是一个可调用的表)。这有一个假设的web路由框架的语法的例子。



match "/post-comment" {
  GET = function ()
    -- render the form
  end,

  POST = function ()
    -- save to database
  end}

If it’s not immediately obvious what’s going on, writing the parenthesis in will clear things up. The precedence of the parenthesis-less invocation goes from left to right, so the above is equivalent to:

如果还是不能立马说明问题,写一个括号就一目了然了。
-->符的调用优先级是从左到右的,与上面的效果一样。


match("/post-comment")({ ... })

The pattern we would use to implement this syntax would look something like this:

这种模式可以实现这个语法,类似于下面这种:



local function match(path)
  print("match:", path)

  return function(params)
    print("params:", params)
    -- both path and params are now availble for use here
  end
end

Using a recursive function constructor it’s possible to make chaining work for any length.

使用递归函数构造,可以是让链变为任意长度。


Using function environments When interacting with a Lua module you regularly have to bring any functions or values into scope using require. When working with a DSL, it’s nice to have all the functionality available without having to manually load anything.

使用函数环境,与lua模块进行交互,你可以require任意一个变量或是函数到作用域。


One option would be to make all the functions and values global variables, but it’s not recommended as it might interfere with other libraries.

可以让全局变量让所有函数都可见,但不推荐这样,可能会影响其它的库。

A function environment can be used to change how a function resolves global variable references within its scope. This can be used to automatically expose a DSL’s functionality without polluting the regular global scope.

函数环境可以用来改变,解决函数作用域范围内的全局变量的引用。

For the sake of this guide I'll assume that setfenv exists in the version of Lua we're using. If you're using 5.2 or above you'll need to provide you own implementation:

这篇我会假设,setfenv已经存在的lua版本中使用。如果你使用的是5.2或是更高版本,你需要自己动手。


Implementing setfenv in Lua 5.2, 5.3, and above Here’s a function run_with_env that runs another function with a particular environment.


local function run_with_env(env, fn, ...)
  setfenv(fn, env)
  fn(...)end

The environment passed will represent the DSL:

环境传递的就是DSL。


local dsl_env = {
  move = function(x,y)
    print("I moved to", x, y)
  end,

  speak = function(message)
    print("I said", message)
  end}

run_with_env(dsl_env, function()
  move(10, 10)
  speak("I am hungry!")end)

In this trivial example the benefits might not be obvious, but typically your DSL would be implemented in another module, and each place you invoke it is not necessary to bring each function into scope manually, but rather activate the whole sscope with run_with_env.

在这平时的例子显示的益处不明显,一般你的DSL可以在其它模块实现,不必要每个地方都调用,让每个函数到作用域里,但是激活整个作用域用run_with_env。

Function environments also let you dynamically generate methods on the fly. Using the __index metamethod implemented as a function, any value can be programmatically created. This is how the HTML builder DSL will be created.

函数环境也让你飞速动态的生成方法。使用__index元方法实现一个函数,任何的变量都可以自动化创建。这是HMTL创建器DSL如何被创建。

Implementing the HTML builder Our goal is to make the following syntax work:


实现HTML构建器,我们的目地就是让下面的语法工作。




html {
  body {
    h1 "Welcome to my Lua site",
    a {
      href = "http://leafo.net",
      "Go home"
    }
  }}


Each HTML tag is represented by a Lua function that will return the HTML string representing that tag with the correct attribute and content if necessary.

每个HTML标签被用一个lua函数表示, 将会返回HTML字符串表示标签正确的属性和正文,如果必要。

Although it would be possible to write code to generate all the HTML tag builder functions ahead of time, a function __index metamethod will be used to generate them on the fly.

虽然它可能提前写代码来生成所有的HTML标签生成器函数, __index函数方法用于快速的生成它。

In order to run code in the context of our DSL, it must be packaged into a function. The render_html function will take that function and convert it to a HTML string:

为了运行我们的DSL正文中代码,它必须打包到函数中。 render_html函数将会把这个函数转换成HTML字符串。



render_html(function()
  return div {
    img { src = "http://leafo.net/hi" }
  }end) -- > <div><img src="http://leafo.net/hi" /></div>

The img tag is self-closing, it has no separate close tag. HTML calls these “void elements”. These will be treated differently in the implementation.

img标签是自动关闭,它没分割符关闭标签。HTML叫这个"空标签"。这个处理会有不现的实现。

render_html might be implemented like this:

render_html实现类似于下面:


local function render_html(fn)
  setfenv(fn, setmetatable({}, {
    __index = function(self, tag_name)
      return function(opts)
        return build_tag(tag_name, opts)
      end
    end
  }))

  return fn()
end

The build_tag function is where all actual work is done. It takes the name of the tag, and the attributes and content as a single table.

build_tag函数是在所有实际工作完成时。它取得标签的名字,属性和正文是一个单个的table。

This function could be optimized by caching the generated functions in the environment table.

这函数是缓冲区优化的,生成的函数在环境table里。

The void elements, as mentioned above, are defined as a simple set:

空元素, 如上所述,定义一个简单的设置:



local void_tags = {
  img = true,
  -- etc...}

The most efficient way to concatenate strings in regular Lua is to accumulate them into a table then call table.concat. Many calls to table.insert could be used to append to this buffer table, but I prefer the following function to allow multiple values to be appended at once:

大多数有效的方式连接字符串在常规的lua中是堆积到table中,当调用table.concat方法时。许多调用talbe.insert可被用于添加到这个缓冲表,但我更喜欢下面的函数,可以允许一次插入多个值。


local function append_all(buffer, ...)
  for i=1,select("#", ...) do
    table.insert(buffer, (select(i, ...)))
  endend

-- example:--


local buffer = {}--   
append_all(buffer, "a", "b", c)-- 
buffer now is {"a", "b", "c"}

append_all uses Lua’s built in function select to avoid any extra allocations by querying the var args object instead of creating a new table.

append_all 使用lua的构建于函数选择避免任何额外分配,用查询的变理参数对象代替创建新表。

Now the implementation of build_tag:

现在是build_tag的实现。


local function build_tag(tag_name, opts)
  local buffer = {"<", tag_name}
  if type(opts) == "table" then
    for k,v in pairs(opts) do
      if type(k) ~= "number" then
        append_all(buffer, " ", k, '="', v, '"')
      end
    end
  end

  if void_tags[tag_name] then
    append_all(buffer, " />")
  else
    append_all(buffer, ">")
    if type(opts) == "table" then
      append_all(buffer, unpack(opts))
    else
      append_all(buffer, opts)
    end
    append_all(buffer, "</", tag_name, ">")
  end

  return table.concat(buffer)end

There are a couple interesting things here:

有两个很有趣的事。

The opts argument can either be a string literal or a table. When it’s a table it takes advantage of the fact that Lua tables are both hash tables and arrays at the same time. The hash table portion holds the attributes of the HTML element, and the array portion holds the contents of the element.

选项参数可以是字符串或是table,当时table时,他高级的事实,lua和table同时是哈希表和数组。哈希表的部分何存HTML元素的属性。

Checking if the key in a pairs iteration is numeric is a quick way to approximate isolating array like elements. It’s not perfect, but will work for this case.

检查如果KEY在一对迭代是数字是一种快速的方式近似隔离数组像元素。并不完美,但是此种情况可适用。



for k,v in pairs(opts) do
  if type(k) ~= "number" then
    -- access hash table key and values
  endend

When the content of the tag is inserted into the buffer for the table based opts, the following line is used:

当标签的内容插入到缓冲区为有表的基础选择,以下:


append_all(buffer, unpack(opts))

Lua’s built in function unpack converts the array values in a table to var args. This fits perfectly into the append_all function defined above.

lua的内建函数unpack转换数组的值到表变量参数。这完美适应上面的append_all函数所定义的。


unpack is table.unpack in Lua 5.2 and above. Closing This simple implementation of an HTML builder that should give you a good introduction to building your own DSLs in Lua.

unpack函数在lua5.2中有,结束这个简单的HTML构建器的实现,应该会给你一个好的介绍在lua中构建你自己的DSL。

The HTML builder provided performs no HTML escaping. It’s not suitable for rendering untrusted input. If you're looking for a way to enhance the builder then try adding html escaping. For example:

HTML生成器提供的是没HTML脱字符,不太适合用于渲染非信认的输入,如果你想通过新方式增强生成器,尝试添加html脱字符,例如:



local unsafe_text = [[<script type="text/javascript">alert('hacked!')</script>]]

render_html(function()
  return div(unsafe_text)end)

-- should not return a functional script tag:-- 
<div>
<script type="text/javascript">alert('hacked!')</script></div>

-->

作者:leafo

DSLs, or domain specific languages, are programming languages that are designed to implement a set of features specific to a particular problem or field. An example could be Make, the build tool, which is a specially designed language for combining commands and files while managing dependencies.
DSLs,“领域特定语言”:是为了特定领域的问题,设计实现了某些功能的编程语言。 * Dropping the parenthesis * Chaining * Using function environments * Implementing the HTML builder * Closing
A lot of modern programming languages have so much flexibility in their syntax that it’s possible to build libraries that expose their own mini-languages within the host language. The definition of DSL has broadened to include these kinds of libraries.
很多现代编程语言,语法非常的灵活,用库的形式,在宿主语言中构他们自己的迷你语言。 用DSL扩展自己的库。
In this guide we'll build a DSL for generating HTML. It looks like this:
这篇我们会用DSL语言生成HTML标记语言,如下: [code] html { body { h1 "Welcome to my Lua site", a { href = "http://leafo.net", "Go home" } }} [/code]
Before jumping in, here are some DSL building techniques:
至此,有一些DSL的构建技术:
Dropping the parenthesis One of the cases for Lua as described in its initial public release(1996) is that it makes a good configuration language. That’s still true to this day, and Lua is friendly to building DSLs.
1996年的lua发行版的描述中就去掉了括号,使他成为一个出色配置语言,延续至今,LUA是友好的DSLs构建语言。
A unique part about Lua’s syntax is parenthesis are optional in some scenarios when calling functions. Terseness is important when building a DSL, and removing superfluous characters is a good way to do that.
唯一lua语法用到括号的场合是函数调用。构建DSL的关键点就是简洁,剔除多余字符也是很好的途径。
When calling a function that has a single argument of either a table literal or a string literal, the parenthesis are optional.
函数调用时的参数是单参数的表字符串或是字符串。括号是可选。 [code] print "hello" --> print("hello")my_function { 1,2,3 } --> my_function({1,2,3}) -- whitespace isn't needed, these also work: 空白符非必须,一样管用。 print"hello" --> print("hello") my_function{ 1,2,3 } --> my_function({1,2,3}) [/code]
This syntax has very high precedence, the same as if you were using parenthesis:
这是个高优先级的语法, 类似于用括号。 [code] tonumber "1234" + 5 -- > tonumber("1234") + 5 [/code]
ChainingParenthesis-less invocation can be chained as long as each expression from the left evaluates to a function (or a callable table). Here’s some example syntax for a hypothetical web routing framework:
-->()运操符调用, 链是从一个左值表达式函数(或是一个可调用的表)。这有一个假设的web路由框架的语法的例子。 [code] match "/post-comment" { GET = function () -- render the form end, POST = function () -- save to database end} [/code]
If it’s not immediately obvious what’s going on, writing the parenthesis in will clear things up. The precedence of the parenthesis-less invocation goes from left to right, so the above is equivalent to:
如果还是不能立马说明问题,写一个括号就一目了然了。 -->符的调用优先级是从左到右的,与上面的效果一样。 [code] match("/post-comment")({ ... }) [/code]
The pattern we would use to implement this syntax would look something like this:
这种模式可以实现这个语法,类似于下面这种: [code] local function match(path) print("match:", path) return function(params) print("params:", params) -- both path and params are now availble for use here end end [/code]
Using a recursive function constructor it’s possible to make chaining work for any length.
使用递归函数构造,可以是让链变为任意长度。
Using function environments When interacting with a Lua module you regularly have to bring any functions or values into scope using require. When working with a DSL, it’s nice to have all the functionality available without having to manually load anything.
使用函数环境,与lua模块进行交互,你可以require任意一个变量或是函数到作用域。
One option would be to make all the functions and values global variables, but it’s not recommended as it might interfere with other libraries.
可以让全局变量让所有函数都可见,但不推荐这样,可能会影响其它的库。
A function environment can be used to change how a function resolves global variable references within its scope. This can be used to automatically expose a DSL’s functionality without polluting the regular global scope.
函数环境可以用来改变,解决函数作用域范围内的全局变量的引用。
For the sake of this guide I'll assume that setfenv exists in the version of Lua we're using. If you're using 5.2 or above you'll need to provide you own implementation:
这篇我会假设,setfenv已经存在的lua版本中使用。如果你使用的是5.2或是更高版本,你需要自己动手。
Implementing setfenv in Lua 5.2, 5.3, and above Here’s a function run_with_env that runs another function with a particular environment.
[code] local function run_with_env(env, fn, ...) setfenv(fn, env) fn(...)end [/code]
The environment passed will represent the DSL:
环境传递的就是DSL。 [code] local dsl_env = { move = function(x,y) print("I moved to", x, y) end, speak = function(message) print("I said", message) end} run_with_env(dsl_env, function() move(10, 10) speak("I am hungry!")end) [/code]
In this trivial example the benefits might not be obvious, but typically your DSL would be implemented in another module, and each place you invoke it is not necessary to bring each function into scope manually, but rather activate the whole sscope with run_with_env.
在这平时的例子显示的益处不明显,一般你的DSL可以在其它模块实现,不必要每个地方都调用,让每个函数到作用域里,但是激活整个作用域用run_with_env。
Function environments also let you dynamically generate methods on the fly. Using the __index metamethod implemented as a function, any value can be programmatically created. This is how the HTML builder DSL will be created.
函数环境也让你飞速动态的生成方法。使用__index元方法实现一个函数,任何的变量都可以自动化创建。这是HMTL创建器DSL如何被创建。
Implementing the HTML builder Our goal is to make the following syntax work:
实现HTML构建器,我们的目地就是让下面的语法工作。 [code] html { body { h1 "Welcome to my Lua site", a { href = "http://leafo.net", "Go home" } }} [/code]
Each HTML tag is represented by a Lua function that will return the HTML string representing that tag with the correct attribute and content if necessary.
每个HTML标签被用一个lua函数表示, 将会返回HTML字符串表示标签正确的属性和正文,如果必要。
Although it would be possible to write code to generate all the HTML tag builder functions ahead of time, a function __index metamethod will be used to generate them on the fly.
虽然它可能提前写代码来生成所有的HTML标签生成器函数, __index函数方法用于快速的生成它。
In order to run code in the context of our DSL, it must be packaged into a function. The render_html function will take that function and convert it to a HTML string:
为了运行我们的DSL正文中代码,它必须打包到函数中。 render_html函数将会把这个函数转换成HTML字符串。 [code] render_html(function() return div { img { src = "http://leafo.net/hi" } }end) -- >
[/code]
The img tag is self-closing, it has no separate close tag. HTML calls these “void elements”. These will be treated differently in the implementation.
img标签是自动关闭,它没分割符关闭标签。HTML叫这个"空标签"。这个处理会有不现的实现。
render_html might be implemented like this:
render_html实现类似于下面: [code] local function render_html(fn) setfenv(fn, setmetatable({}, { __index = function(self, tag_name) return function(opts) return build_tag(tag_name, opts) end end })) return fn() end [/code]
The build_tag function is where all actual work is done. It takes the name of the tag, and the attributes and content as a single table.
build_tag函数是在所有实际工作完成时。它取得标签的名字,属性和正文是一个单个的table。
This function could be optimized by caching the generated functions in the environment table.
这函数是缓冲区优化的,生成的函数在环境table里。
The void elements, as mentioned above, are defined as a simple set:
空元素, 如上所述,定义一个简单的设置: [code] local void_tags = { img = true, -- etc...} [/code]
The most efficient way to concatenate strings in regular Lua is to accumulate them into a table then call table.concat. Many calls to table.insert could be used to append to this buffer table, but I prefer the following function to allow multiple values to be appended at once:
大多数有效的方式连接字符串在常规的lua中是堆积到table中,当调用table.concat方法时。许多调用talbe.insert可被用于添加到这个缓冲表,但我更喜欢下面的函数,可以允许一次插入多个值。 [code] local function append_all(buffer, ...) for i=1,select("#", ...) do table.insert(buffer, (select(i, ...))) endend [/code] -- example:-- [code] local buffer = {}-- append_all(buffer, "a", "b", c)-- buffer now is {"a", "b", "c"} [/code]
append_all uses Lua’s built in function select to avoid any extra allocations by querying the var args object instead of creating a new table.
append_all 使用lua的构建于函数选择避免任何额外分配,用查询的变理参数对象代替创建新表。
Now the implementation of build_tag:
现在是build_tag的实现。 [code] local function build_tag(tag_name, opts) local buffer = {"<", tag_name} if type(opts) == "table" then for k,v in pairs(opts) do if type(k) ~= "number" then append_all(buffer, " ", k, '="', v, '"') end end end if void_tags[tag_name] then append_all(buffer, " />") else append_all(buffer, ">") if type(opts) == "table" then append_all(buffer, unpack(opts)) else append_all(buffer, opts) end append_all(buffer, "") end return table.concat(buffer)end [/code]
There are a couple interesting things here:
有两个很有趣的事。
The opts argument can either be a string literal or a table. When it’s a table it takes advantage of the fact that Lua tables are both hash tables and arrays at the same time. The hash table portion holds the attributes of the HTML element, and the array portion holds the contents of the element.
选项参数可以是字符串或是table,当时table时,他高级的事实,lua和table同时是哈希表和数组。哈希表的部分何存HTML元素的属性。
Checking if the key in a pairs iteration is numeric is a quick way to approximate isolating array like elements. It’s not perfect, but will work for this case.

检查如果KEY在一对迭代是数字是一种快速的方式近似隔离数组像元素。并不完美,但是此种情况可适用。

[code] for k,v in pairs(opts) do if type(k) ~= "number" then -- access hash table key and values endend [/code]

When the content of the tag is inserted into the buffer for the table based opts, the following line is used:
当标签的内容插入到缓冲区为有表的基础选择,以下: [code] append_all(buffer, unpack(opts)) [/code]
Lua’s built in function unpack converts the array values in a table to var args. This fits perfectly into the append_all function defined above.

lua的内建函数unpack转换数组的值到表变量参数。这完美适应上面的append_all函数所定义的。

unpack is table.unpack in Lua 5.2 and above. Closing This simple implementation of an HTML builder that should give you a good introduction to building your own DSLs in Lua.
unpack函数在lua5.2中有,结束这个简单的HTML构建器的实现,应该会给你一个好的介绍在lua中构建你自己的DSL。
The HTML builder provided performs no HTML escaping. It’s not suitable for rendering untrusted input. If you're looking for a way to enhance the builder then try adding html escaping. For example:

HTML生成器提供的是没HTML脱字符,不太适合用于渲染非信认的输入,如果你想通过新方式增强生成器,尝试添加html脱字符,例如:

[code] local unsafe_text = [[]]

renderhtml(function() return div(unsafetext)end)

-- should not return a functional script tag:--

[/code]


糖果实验室

Openresty中文编程网
IKBC经典机械键盘
机械键盘领券优惠购买

Write a Comment