为什么使用元编程而不是函数?

huangapple go评论54阅读模式
英文:

Why metaprogramming instead of functions?

问题

我是一个JavaScript程序员,对Lisp充满兴趣,因为听到像Eric Raymond这样的人声称它经常是启发性的。免责声明:我确信我在某些方面是个新手,也有很多我不理解的东西。

到目前为止,我觉得Lisp的真正好处在于它使元编程更容易。因此,我试图理解为什么元编程如此有用。但正如Matthew Butterick所谈论的那样,人们并没有真正给出为什么它有用的具体例子。更多的是“你必须学一些Lisp并自己看看”。

我对此有些怀疑。我不明白为什么不能提供一些例子。因此,我一直在寻找。不幸的是,我没有找到太多。对于我找到的例子,我总是在想“这不就是一个函数吗?”

Reddit上的这个lif例子可能是最好的,也是最容易谈论的。简而言之,不是这样做:

(let ([x (get-x)])
  (if (valid? x) 
      (use-x x) 
      (error "no x available" x)))

你可以写一个宏,允许你这样做:

(lif [x (get-x)]
     (valid? x)
     (use-x x) 
     (error "no x available" x))

这里有个问题:为什么是宏而不是函数?对我来说,lif看起来像一个函数,接受get-xvalid?use-xerror"no x available"作为输入,并给你任何输出。

英文:

I'm a JavaScript programmer who is very intrigued by Lisp after hearing people like Eric Raymond claim that is often enlightening. Disclaimer: I'm sure there are various ways I'm being a noob and various things I'm just not understanding.

So far it seems to me like the big benefit to Lisp really comes down to the fact that it makes metaprogramming easier. So then, I'm trying to understand why metaprogramming is so useful. But as Matthew Butterick talks about, people don't really give concrete examples of why it is useful. It's more "you're going to have to learn some Lisp and see for yourself".

I'm a little skeptical of that though. I don't see why examples can't be provided. And so, I've been searching for them. I haven't come across much, unfortunately. For the examples I have come across, I always find myself saying "Can't this just be a function?"

This lif example from Reddit is probably the best and easiest to talk about. In short, instead of doing this:

(let ([x (get-x)])
  (if (valid? x) 
      (use-x x) 
      (error "no x available" x)))

You can write a macro that allows you to do this:

(lif [x (get-x)]
     (valid? x)
     (use-x x) 
     (error "no x available" x))

Here's my question: why a macro instead of a function? To me, lif looks like a function that takes get-x, valid?, use-x, error and "no x available" as inputs and gives you whatever as the output.

答案1

得分: 3

元编程有许多不同的形式。
通过宏进行的元编程只是其中一种形式。

使用宏进行元编程意味着通过源代码转换来扩展编程语言。宏接受源代码并输出新的源代码,可以在编译时或运行时进行。Lisp中的宏被设计成可以在编译期执行。

让我们来看看源代码

如果在Lisp中有这样一个表达式,那么它有哪些部分?

(let ((x (get-x))) (if (valid-p x) (use-x x) (error "no x available" x)))

LET是一个内置的特殊运算符,具有自己的求值规则。它不是一个函数。

IF也是一个内置的特殊运算符,具有自己的求值规则。它不是一个函数。

GET-XVALID-PUSE-XERROR可能是函数。

X是一个变量,在LET绑定中引入。

因此,(LET ...)(IF ...) 都不是函数调用。

请记住,在Lisp中求值一个函数调用的规则:

  • 对所有参数表达式求值,得到它们的值。
  • 找到函数。
  • 使用这些值调用函数。
  • 执行函数代码。
  • 从函数调用中返回结果。

LET不会对((x (get-x)))进行求值。它将其视为一个绑定列表。X是一个变量。(GET-X)是一个形式。(x (get-x))是一个绑定列表。对形式(GET-X)进行求值,将新的词法变量X绑定到结果。
然后在这个词法范围内求值LET形式的所有主体表达式。

一个新的控制结构

现在,你的LIF也不是一个函数,因为它也使用了不同的求值方式:

(lif (x (get-x)) (valid? x) (use-x x) (error "no x available" x))

(x (get x))不会被求值。

(use-x x)(error ...)根据第二个子表达式(valid-p ...)的求值结果进行求值。

因此,这不是一个函数调用。

现在,LIF不是内置的,但通过宏,我们可以将LIF 形式转换为有效的Lisp形式,就像我们在IFLET中看到的那样。
形式是一个用于求值的表达式。

这使您能够在用户/程序员级别扩展编程语言的语法语义LIF宏接受可能不是有效的Lisp代码的代码,并将其转换为其他东西,最终应该是有效的Lisp代码。

现在,你只需要问自己,可以用这个进行什么样的转换。实际上,答案取决于程序员的想象力。起初,人们希望编写容易并简单地适应语言的宏。但后来,我们也可以弯曲规则和约定。

Common Lisp中的一个示例是LOOP宏:

(LOOP FOR i FROM 0 BY 3
      REPEAT 10

      WHEN (evenp i)
       COLLECT i)

-> (0 6 12 18 24)

它的目的是a) 成为一个专注于迭代模式的子语言,b) 它应该扩展为高效的Lisp代码(手动编写这样的代码不容易,也不容易阅读)。但它的奇怪之处(以及具有争议的地方)在于,它在Lisp中引入了大量语法,代码看起来与普通的带有大量括号的Lisp不太相似。上面的LOOP形式会扩展成一整页的低级代码。LOOP的实现复杂(-> SBCL LOOP implementation,看看它包含更多的宏实现以实现LOOP宏,-> 我们也可以在宏中使用宏),实际上它是用Lisp编写的编译器,用于嵌入式语言,Lisp本身是目标语言。

因此,宏的一个目的是实现嵌入式特定领域子语言,这里是迭代的新语言。

英文:

Meta programming comes in many different forms.
Meta programming via macros is just one Flavor.

Meta programming with macros means extending the programming language via source code transformations. A macro takes source code and outputs new source code - either at compile time or during runtime. Macros in Lisp are designed such that they can be executed during compilation.

Let's look at the source

If we have an expression like this in Lisp, then what are the parts?

(let ((x (get-x)))
  (if (valid-p x) 
      (use-x x) 
    (error "no x available" x)))

LET is a built-in special operator with its own evaluation rules. It is not a function.

IF is a built-in special operator with its own evaluation rules. It is not a function.

GET-X, VALID-P, USE-X and ERROR might be functions.

X is a variable, introduced with a LET binding.

Thus neither (LET ...) nor (IF ... ) are function calls.

Remember the rule to evaluate a function call in Lisp:

  • evaluate all the argument expressions to their values

  • find the function

  • call the function with the values

  • execute the function code

  • return a result from the function call

The LET does not evaluate ((x (get-x))) . It treats it as a binding list. X is a variable. (GET-X) is a form. (x (get-x)) is a binding list. Evaluate the form (GET-X) and bind the new lexical variable X to the result.
The evaluate all the body expressions of the LET form in this lexical scope.

a new control structure

Now your LIF is also not a function, because it also uses different evaluation:

(lif (x (get-x))
       (valid? x)
     (use-x x) 
  (error "no x available" x))

(x (get x)) does not get evaluated.

(use-x x) and (error ...) are getting evaluated based on the evaluation result of the second sub-expression (valid-p ...).

Thus this can't be a function call.

Now, LIF is not built-in, but with a macro we can transform the LIF form into a valid Lisp form, the form we saw about with the IF and LET.
A form is an expression meant to be evaluated.

That lets you expand the syntax and the semantics of your programming language on a user/programmer level. The LIF macro takes code which might not be valid Lisp code and transforms it into something else, which eventually should be valid Lisp code.

Now you only need to ask yourself, what kind of transformations one can do with this. The answer actually is. that this is left to the imagination of the programmer. At first one wants to write macros which easily and simply fit into the language. But then we can also bend the rules&conventions.

An example in Common Lisp is the LOOP macro:

(LOOP FOR i FROM 0 BY 3
      REPEAT 10

      WHEN (evenp i)
       COLLECT i)

-> (0 6 12 18 24)

Its purpose is to a) be a sublanguage that is focused on iteration patterns and b) it should expand into efficient Lisp code (which would not be easy to be written manually and which would not be very readable). But what is strange (and controversial) about it, is that it introduces a lot of syntax into Lisp and the code does not look like the normal Lisp with a lot of parentheses. The LOOP form above expand into a page of lower-level code. The implementation of LOOP is complex (-> SBCL LOOP implementation, see how it includes even more macros implementations to implement the LOOP macro -> we can use macros in macros, too) and in effect it is a compiler written in Lisp, for an embedded language, with Lisp itself being the target language.

Thus ONE purpose of a macro is to implement embedded domain specific sublanguages, here a new language for iteration.

答案2

得分: 2

这就是元编程有用的原因。在我们宇宙的一个变种中,时间是1958年(因此特别说明,没有一切都像这样发生),我正在基于λ-演算发明一种编程语言。该语言的纸质版本中的函数如下所示:

λ x y z: ... 一些内容 ...

但是现在是1958年,解析器还没有真正被发明,更不用说Unicode了(我会假装小写字母已经被发明)。因此,我发明了一个简单的语法,以及一个内存中的数据表示和一个读取器和打印器,我称之为s-表达式,并将其用于表示我的语言。上面的函数随后表示为

(lambda (x y z) ... 一些内容 ...)

好吧,我现在可以将其读入内存并使用我用机器码编写的程序来评估它,该程序遍历此数据表示。实际上,我已经发明了Lisp。

嗯,在我的原始语言的文本形式中,函数应用可能如下所示:

x y z: ... 一些内容 ...)(thing 1 thing 2 thing 3)

比如,我将其转化为我的s-表达式表示如下:

((lambda (x y z) ...) 1 2 3)

现在,我开始编写这种语言的第一个程序,发现局部变量很有用。好吧,我可以使用局部变量:

((lambda (x)
   ... 这里x是一个局部变量 ...)
 1)

但这很难阅读,特别是如果代码很多:

((lambda (x)
   ... 300行 ...
   ...)
 1)

这真的很痛苦,因为要查看x的初始值,我必须查看5页的输出。我希望能够编写一些使程序更容易阅读的东西。我想,好吧,我将不得不修改我的评估器函数(它是用机器码编写的),这将非常痛苦,特别是因为我还不确定新语法应该是什么。

然后,灵光一现:我的程序的源代码是一种数据结构,即s-表达式,我可以编写一个程序,将具有良好语法的s-表达式转化为难以阅读的s-表达式。具体来说,我想出了以下函数:

(lambda (form)
  (append
   (list
    (cons (quote lambda)
          (cons
           (mapcar (lambda (binding)
                     (cond
                      ((symbolp binding)
                       binding)
                      ((listp binding)
                       (car binding))))
                   (cadr form))
           (cddr form))))
   (mapcar (lambda (binding)
             (cond
              ((symbolp binding) (quote nil))
              ((listp binding) (cadr binding))))
           (cadr form))))

如果我将此函数命名为let-expander,我可以尝试将其应用于我决定要使用的语法的s-表达式:

> (let-expander (quote (let ((x 1) (y 2))
                         (+ x y))))
((lambda (x y) (+ x y)) 1 2)

因此,现在,我可以编写具有如下写法的局部变量的程序:

(let ((x 1) ...)
  ...)

将它们传递给我的let-expander函数,然后只需使用我的旧评估器来评估生成的s-表达式。

我刚刚发明了一种新语言:这是一种类似于我的旧语言但具有这种新的let构造的语言。在这种新语言中阅读程序要容易得多!(而let不能是一个函数:它的第一个参数是一个变量和值的列表,而不是要评估的内容:let是一种语言的扩展,不是其中定义的函数。)

但等等:我不必停在这里,对吗?每当我决定在我的语言中加入一个新功能时,我可以编写其中一个这些'扩展器'函数,现在我有了这个功能。我还可以使用由以前的所有扩展器扩展的语言来编写新的扩展器,如果我小心的话。

不久,我编写了一种类似于元扩展器的东西,它知道这些扩展器的表格,并遍历s-表达式,忠实地调用扩展器,直到没有可扩展的内容。我将其添加为我的评估器的前端。

这一切的最终结果是令人惊奇的:我从一个具有lambdacondmapcar(我可以在语言本身中编写)以及一些用于处理和构建这些s-表达式对象的原始语言开始,突然之间我拥有了无限多种语言。其中一些具有额外的通用构造,如ifcasewhenunless等,我只需使用扩展器来实现,这些构造变得更多或更少地标准。其他语言具有特殊扩展,以处理特定领域的事物:用于第一次木星任务的语言有很多看起来像这样的代码:

(with-ensured-spacecraft-state (:pyros 'armed :ullage-motors 'ready)
  ...
  (fire-pyros 1 2 4)
  ...
  (ullage-motors 2.8)
  ...)

这些结构显然对系统的安全性至关重要:只有在满足前提条件

英文:

Here is why metaprogramming is useful. It is 1958 in a variant of our universe (so, in particular, none of this happened quite like this) and I am inventing a programming language based on λ-calculus. Functions in the paper version of this language look like

> λ x y z: ... something ...

But it's 1958 and parsers haven't really been invented, let alone unicode (I will pretend that lower-case has been invented). So I invent a simple syntax, together with an in-memory data representation and a reader and printer, which I call s-expressions and use this to represent my language. The above function is then represented as

(lambda (x y z) ... something ...)

Well, I can now read this into memory and evaluate it using a program I've written in machine code which walks this data representation. I have, in fact, invented Lisp.

Well, in the textual form of my original language function application might look like

> (λ x y z: ... something ...)(thing 1 thing 2 thing 3)

say, and I turn this into my s-expression representation as

((lambda (x y z) ...) 1 2 3)

say.

So now I start to write the first programs in this language, and one thing I discover is that local variables are useful. Well, I can do local variables:

((lambda (x)
   ... x is a local variable here ...)
 1)

But this is hard to read, especially if there is lots of code:

((lambda (x)
   ... 300 lines ...
   ...)
 1)

is just painful, because to see what the initial value of x is I have to look through 5 pages of printout. I want to be able to write something that makes it easier to read my program. I think, well, I will have to modify my evaluator function (which is in machine code) and this will be very painful, especially as I am not yet sure what the new syntax should be.

Then the light goes on: the source code of my program is a datastructure – an s-expression – and I can write a program to take an s-expression in the nice syntax and turn it into one in the hard-to-read one. In particular I come up with this function:

(lambda (form)
  (append
   (list
    (cons (quote lambda)
          (cons
           (mapcar (lambda (binding)
                     (cond
                      ((symbolp binding)
                       binding)
                      ((listp binding)
                       (car binding))))
                   (cadr form))
           (cddr form))))
   (mapcar (lambda (binding)
             (cond
              ((symbolp binding) (quote nil))
              ((listp binding) (cadr binding))))
           (cadr form))))

And if I name this function let-expander I can try it on an s-expression using a syntax which I've decided I want to use:

> (let-expander (quote (let ((x 1) (y 2))
                         (+ x y))))
((lambda (x y) (+ x y)) 1 2)

So now, I can write programs which have local variables written as

(let ((x 1) ...)
  ...)

feed them through my let-expander function and then just use my old evaluator to evaluate the resulting s-expression.

I have just invented a new language: a language which is like my old language but has this new let construct in it. It is much easier to read programs in this new language! (And let can't be a function: its first argument is a list of variables and values, not something to be evaluated: let is an extension to the language, not a function defined in it.)

But wait: I don't have to stop there, do I? Every time I decide I want a new feature in my language I can write one of these 'expander' functions and now I have that feature. I can also use the language extended by all the previous expanders to write new ones if I'm careful.

Pretty soon I write a sort of meta-expander which knows about a table of these expanders and walks over an s-expression dutifully calling the expanders until there is nothing left to expand. I add this as a front-end to my evaluator.

The end result of this is something completely extraordinary: I started with a primitive language which had lambda, cond, mapcar (which I can write in the language itself), and some primitives for dealing with and building these s-expression objects, and suddenly I have an infinite family of languages. Some of these have extra general-purpose constructs, like if and case and when and unless and so on, all of which I can just implement using expanders and which become more-or-less standard. Others have special extensions to deal with domain-specific things: the language used for the first Jupiter missions had a lot of code that looked like:

(with-ensured-spacecraft-state (:pyros 'armed :ullage-motors 'ready)
  ...
  (fire-pyros 1 2 4)
  ...
  (ullage-motors 2.8)
  ...)

Constructs like this were obviously critical to the safety of the system: the code in the body of this form could not run unless the preconditions were met. Sadly we did not anticipate the alien incursion however.

And all this came from one thing (apart from the aliens): I represented the source code of my programs in an extremely simple data structure (in particular that data structure was not full of assumptions about what things meant) which was available to the programs themselves.

This is why metaprogramming is useful: metaprogramming is building programming languages.


Notes:

  • 'expanders' are macros, and 'expander functions' are macro functions in CL;
  • in Common Lisp let is a 'special operator' – something built into the evaluator – but it does not have to be and is not in my variant world:
  • same for if.

答案3

得分: 1

如果你已经看过ReactJS,那么你已经见过使用元编程可以实现的与常规函数不同的示例。元编程在JavaScript社区中非常常见。有大量“编译成JavaScript”的语言,它们都是元编程的示例。

元编程允许你做一些否则需要新编译器才能实现的事情。

内置宏的优势在于你不必编写外部工具来支持类似ReactJS的替代语法。如果有人要实现ReactLisp,只需要编写一些读取宏,将组件语法解析为普通的Lisp代码。

读取宏是返回Lisp代码的函数,因此不需要将临时文件写入磁盘。

它集成得如此之好,甚至不需要将ReactLisp源文件与普通的Lisp文件分开(在React.js项目中,使用.jsx扩展名而不是.js来实现这一点)。

除了读取宏,Lisp还有AST宏,可以用来实现不那么激进的语法扩展。例如,多年前,JavaScript社区的某人提出了一些叫做“promises”的东西,你必须在它们上调用then方法来执行下一步操作。

然后,因为使用promises非常繁琐,他们决定在JavaScript中添加asyncawait关键字。它们只是用于Promises周围习惯的语法糖。

你可以在Lisp中实现promises(但请注意,Scheme中有一种叫做“promise”的东西,与JavaScript中称之为promise的构造没有任何关系),然后使用内置到Lisp中的asyncawait宏来扩展语言,这些宏将与JavaScript内置的asyncawait关键字一样有效。由于宏内置到Lisp中,所以无需更改语言。这个扩展可以是一个库。

英文:

If you've seen ReactJS, you've already seen an example of what you can do with metaprogramming that you can't do with regular functions. Metaprogramming is very common in the JS community. There is a plethora of "compile-to-JavaScript" languages, and they're all examples of metaprogramming.

Metaprogramming allows you to do things that would otherwise require a new compiler.

The advantage you get from having macros built into the language is that you don't have to write an external tool to support alternative syntax like that of ReactJS. If someone was to implement ReactLisp, it would only be necessary to write some reader macros that would parse the component syntax into ordinary Lisp code.

Reader macros are functions that return Lisp code, so there's no need to write temporary files to disk.

It's so well integrated that it wouldn't even be necessary to separate ReactLisp source files from ordinary Lisp files (which you do in a React.js project by using the .jsx extension instead of .js).

In addition to reader macros, Lisp also has AST macros, which can be used to implement less radical syntax extensions. For example, years ago, someone in the JavaScript community came up with these things called "promises", and you had to call the then method on them to carry out the next step.

Then, because using promises was so cumbersome, they decided to add the async and await keywords to JavaScript. They're just syntactic sugar for the idioms around Promises.

You could implement promises in Lisp (but be advised that there's something called a "promise" in Scheme, and it has nothing to do with the construct that JavaScript calls a promise), and then extend the language with async and await macros which would work just as well as the async and await keywords that are built into JavaScript. Since macros are built into Lisp, there's no need for there to be a change to the language. The extension could be a library.

答案4

得分: 0

以下是代码部分的中文翻译:

所以想象一下您可以创建一个这样的函数

(define flif (bind pred consequence alternative)
  (let ([x bind])
    (if (pred x)
        (consequence x)
        (alternative x))))

我喜欢使用这个示例

(lif [x (get-x)]
     (< x 10)
     (if (< x 0) 0 x) 
     (- x 10))

在flif中您需要绕过一些障碍因为if的各个部分都不是单参数函数它看起来像这样

(lif [x (get-x)]
     (lambda (x) (< x 10))
     (lambda (x) (if (< x 0) 0 x)) 
     (lambda (x) (- x 10)))

现在您基本上让这个小而不是很复杂的用法变得如此麻烦我宁愿考虑使用`let``if`

(let ([x (get-x))
  (if (< x 10)
      (if (< x 0) 0 x) 
      (- x 10)))

宏是语法糖在JavaScript中我们有TC39可以提出新功能它们是通过在Babel中实现它们作为插件来做到这一点的例如`async`/`await`就是这样引入的这是一个非常简单的事情

```javascript
async function test () {
   const x = await expression
   return x + 3;
}

// 没有async/await的相同功能
function test () {
   return expression.then((x) => {
     return x + 3;
   })
}

这个想法是程序员可以更流畅地编写代码,而底层实际上执行了这些嵌套的promise操作。但是所有的async函数始终返回promises,因为魔术只在async函数内部生效。

我应该提到,async/await增加了一些错误处理,如果expression不变成promise,它会将该值包装在promise中并解析它。

我敢说你无法提出一个比只使用then更简单的替代方案,而不使用宏/插件。这是不可能的。

JavaScript有TC39,因为它不支持宏。我相信,如果我们提出一种在语言中定义宏的方法,TC39将不再需要超过90%的建议,因为每个人都可以导入一个库,提供您使用的功能。


<details>
<summary>英文:</summary>

So imagine you make it a function like this:

    (define flif (bind pred consequence alternative)
      (let ([x bind])
        (if (pred x)
            (consequence x)
            (alternative x))))

I like to use this example instead:

    (lif [x (get-x)]
         (&lt; x 10)
         (if (&lt; x 0) 0 x) 
         (- x 10))

In flif you need to jump thru hoops since none of the if parts are one argument functions. It looks like this:

    (lif [x (get-x)]
         (lambda (x) (&lt; x 10))
         (lambda (x) (if (&lt; x 0) 0 x)) 
         (lambda (x) (- x 10)))

And now you&#39;re basically making this little and not very complex use so troublesome I would really rather considering using `let` and `if`:

    (let ([x (get-x))
      (if (&lt; x 10)
          (if (&lt; x 0) 0 x) 
          (- x 10)))

Macros are syntax sugar. In Javascript we have TC39 where new features are suggested. How they do that is by implementing them in babel as a plugin. Eg. `async` / `await` was introduced this way. It&#39;s a really simple thing:

&lt;!-- language: lang-js --&gt;    

    async function test () {
       const x = await expression
       return x + 3;
    }
    
    // same without async / await
    function test () {
       return expression.then((x) =&gt; {
         return x + 3;
       })
    }

The idea is that the programmer can write the code more streamlined while under the hood it really did these nasty nested promise stuff. However all async functions ALWAYS return promises since the magic is limited within a async function. 


I should mention that async/await adds some error handling and in the event `expression` does not become a promise it wraps the value in a promise and resolves it. 

I dare you to come up with a replacement that is simpler than just using `then` without using macros/plugins. It cannot be done.

JavaScript has TC39 because it does not support macros. I belive if we made a strawman adding a way to define macros in the language, TC39 would not be needed in over 90% of the suggestions since everyone can just import a library giving you the feature to use. 

</details>



huangapple
  • 本文由 发表于 2023年3月12日 14:13:07
  • 转载请务必保留本文链接:https://go.coder-hub.com/75711365.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定