简体   繁体   English

编写递归模板haskell函数

[英]Writing recursive template haskell functions

Is it possible to transform a recursive TH function into an equivalent form which will compile? 是否有可能将递归TH函数转换为将编译的等效形式? The following definition doesn't work, because in order to compile fact you must first compile fact . 以下定义不起作用,因为为了编译fact您必须首先编译fact

fact :: ExpQ -> ExpQ
fact n = [|
    case $n of 
      1 -> 1
      _ -> $n * $(fact [| $n - 1 |]) |]

This simple example is easily solved ( fact n = [| product [ 1.. $n] |] ) but in the general case, if it is not possible to rewrite a given function as a loop, can a recursive TH function be defined? 这个简单的例子很容易解决( fact n = [| product [ 1.. $n] |] )但是在一般情况下,如果不能将给定函数重写为循环,可以定义递归TH函数? Is there even a single example for which this doable? 是否还有一个可行的例子?

To clarify for future readers: this question is specifically about writing recursive TH functions - not about 'how do I splice the factorial function'. 为了澄清未来的读者:这个问题具体是关于编写递归 TH函数 - 而不是'如何拼接阶乘函数'。

The answer to my question turned out to be remarkably simple: 我的问题的答案非常简单:

{-# LANGUAGE TemplateHaskell #-}

import Control.Monad.Fix (fix)
import Language.Haskell.TH

fact = [| \f x -> $([| 
     case x of 
       1 -> 1 
       _ -> f $([| x - 1 |]) * x  |]) |]

factorial = [| fix $fact |]

fact can be compiled because it is no longer recursive, and [| fix $fact |] fact可以编译,因为它不再是递归的, [| fix $fact |] [| fix $fact |] is compiled at a later time, so no more infinite recursive definitions. [| fix $fact |]是在以后编译的,所以不再有无限的递归定义。

This version of fact looks slightly different than the original, but you can write the new fact exactly as the old one and transform it later: 这个版本的fact看起来与原版略有不同,但是您可以将旧fact完全与旧fact编写并稍后进行转换:

fact' recurse n = [|
        case $n of 
          1 -> 1
          _ -> $n * $(recurse [| $n - 1 |]) |]

fact = [| \x -> $((\f -> [| \x -> $(fact (\x -> [| $f $x |]) [| x |]) |]) [| x |]) |]

The fundamental problem with your code is not that it is recursive, but that it doesn't terminate. 代码的根本问题不在于它是递归的,而是它不会终止。 The n argument to fact just keeps getting bigger and bigger because [| $n - 1 ]| factn论证不断变得越来越大,因为[| $n - 1 ]| [| $n - 1 ]| is an expression tree representing the operation (-) applied to n and 1 . 是表示应用于n1的操作(-)的表达式树。

Any non-terminating splice will hang the compiler in just the same way, for example the following behaves just like your fact when spliced: 任何非终止拼接都会以相同的方式挂起编译器,例如,以下行为就像拼接时的fact一样:

broken :: ExpQ -> ExpQ
broken n = return $ LitE (IntegerL (fromIntegral (length [1..])))

A recursive function where the recursion is guaranteed to bottom out is guaranteed to terminate and works fine for appropriate inputs: 保证递归递归的递归函数保证终止并适用于适当的输入:

fact1 :: ExpQ -> ExpQ
fact1 n = do
    nBody <- n
    case nBody of
      LitE (IntegerL 1) -> [|1|]
      LitE (IntegerL nInt) | nInt > 1 ->
          let nMinusOne = return $ LitE (IntegerL (nInt-1))
          in [| $n * $(fact1 nMinusOne) |]

but of course it fails if the input isn't an appropriate integer literal. 但是,如果输入不是合适的整数文字,它当然会失败。

You can also shift the recursion to runtime, so that instead of the recursive call being with an ever-bigger expression tree, it's with the runtime evaluated and shrinking Int : 您还可以将递归转换为运行时,以便不再使用更大的表达式树进行递归调用,而是使用运行时评估和收缩Int

fact2 :: ExpQ -> ExpQ
fact2 n =
   [|
    let factImpl n =
         case n of
          1 -> 1
          _ -> n * factImpl (n-1)
    in factImpl $n
   |]

Of course in this code we're not doing any analysis of the structure of n . 当然在这段代码中我们没有对n的结构进行任何分析。 But we can put it together with fact1 to get a version that is compile-time executed in some cases and defers others to runtime: 但我们可以将它与fact1放在一起,以获得在某些情况下执行编译时的版本,并将其他版本推迟到运行时:

fact3 :: ExpQ -> ExpQ
fact3 n = do
    nBody <- n
    case nBody of
      LitE (IntegerL 1) -> [|1|]
      LitE (IntegerL nInt) ->
          let nMinusOne = return $ LitE (IntegerL (nInt-1))
          in [| $n * $(fact3 nMinusOne) |]
      _ -> [|
            let factImpl n =
                  case n of
                    1 -> 1
                    _ -> n * factImpl (n-1)
            in factImpl $n
           |]

Ultimately in your real code, you will need to apply some combination of these techniques - make sure that your compile-time recursion terminates and defer any remaining cases to runtime evaluation somehow. 最终在您的实际代码中,您将需要应用这些技术的某些组合 - 确保您的编译时递归终止并将任何剩余的情况推迟到运行时评估。

Yes, you can by using the following: 是的,您可以使用以下方法:

fact :: Int -> ExpQ
fact 0 = [| 1 |]
fact n = [| $(lift n) * $(fact $ n - 1) |]

lift is a function inside Language.Haskell.TH.Lift which converts a basic haskell values into template haskell values (eg Int to ExpQ ). liftLanguage.Haskell.TH.Lift中的一个函数,它将基本的haskell值转换为模板haskell值(例如IntExpQ )。

Note that you don't need the case code to be generated, as you know the number at compile time. 请注意,您不需要生成案例代码,因为您知道编译时的数字。 The above macro will expand to a series of multiplications. 上面的宏将扩展为一系列乘法。 Eg $(fact 4) will expand to 4*3*2*1 . 例如$(fact 4)将扩展到4*3*2*1

Note that in this case, you can do much better though. 请注意,在这种情况下,您可以做得更好。 A template haskell expression is run at compile time, so a template haskell fact function can just return the literal value it represents. 模板haskell表达式在编译时运行,因此模板haskell fact函数只能返回它所代表的文字值。 Eg $(fact 4) can return 24 (instead of 4*3*2*1 ). 例如$(fact 4)可以返回24 (而不是4*3*2*1 )。 This can be done with the following code: 这可以使用以下代码完成:

fact2 :: Int -> ExpQ
fact2 n = lift (product [1..n])

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM