【问题标题】:f# Computation expressions for code generationf# 代码生成的计算表达式
【发布时间】:2013-06-25 14:17:14
【问题描述】:

虽然人们找到了一些关于如何使用 f# 计算表达式进行组合递归下降解析器的示例,但我尝试将它们用于相反的情况。创建易于阅读的代码以从一些 XML 数据生成 (c++) 源文件。然而,我被困住了,如果社区能帮助我找到我的误解,我将不胜感激。为了公众利益,我希望这篇文章很快会展示如何通过 f# 计算表达式,monadic 风格以一种很酷的方式来做代码生成器。

这是我到目前为止的进展情况(简化,为了这个问题的目的,省略了生成的输入数据):

// in my full fledged application, State type also contains the Input data, used for generating code.
type State() = 
    let builder = new System.Text.StringBuilder()
    let mutable indentLevel : int = 0
    member this.Result() = builder.ToString()
    member this.Emit (s : string) : unit = builder.Append( s )
    // ... Methods allowing to do the indenting right, using indentLevel. And adding Output to the builder instance.
    member this.Indent() = indentLevel <- indentLevel + 1
    member this.Exdent() = indentLevel <- indentLevel - 1
// The return value of the Formatters is State only to allow for |> pipelining.
type Formatter = State -> State
type FormatterBuilder() = 
    // Q: Bind() Kind of Looks wrong - should it be a generic, taking one generic first Parameter? See Class function below.
    member this.Bind (state,formatter) = formatter state
    member this.Return state = state              // Q: Not sure if this is the way to go. Maybe some Lambda here?!

let format = new FormatterBuilder()

// Q: Now Comes the part I am stuck in!
// I had the idea to have a "Block" function which 
// outputs the "{", increases the indent Level, 
// invokes the formatters for the Content of the block, 
// then reduces the indent Level, then Closes "}". 
// But I have no idea how to write this.
// Here my feeble attempt, not even sure which Parameters this function should take.
let rec Block (formatters : Formatter list) (state : State) : State =
    format 
        {
            state.EmitLine("{") // do I Need a "do!" here?
            state.Indent()
            formatters |> List.iter (fun f -> do! f state) // Q: "state" is not really propagated. How to do this better?
            state.Exdent()
            state.EmitLine "}"
        }
// Functions with "Get" prefix are not shown here. They are supposed to get the Information
// from the Input, stored in State class, which is also not shown here.
let rec Namespace (state : State) : State =
    format
        {
             state.EmitLine(GetNameSpace state)
        }
let rec Class (classNode : XmlNode) (state : State) : State =
     Format
        { 
             do! TemplateDecl classNode state   // TemplateDecl function not shown in sample code
             do! ClassDecl classNode state
             do! Block [ NestedTypes classNode; Variables classNode; // ... ] // just to give the idea. Q: the list seems wrong here - how to do it better? 
        }
let GenerateCode() : string = 
     let state = new State()
     format
         {
             do! Namespace state    // Q: Is there a way to get rid of the passing of state here?
             do! Block 
                [   // Q: Maybe a Seq is better than a list here?
                 for c in State.Classes do // Q: requires override of a few functions in Builder class, I guess?!
                  do! Class c state
                ]
         }    
     state.Result()

显然,上面的代码最多只能显示我试图实现的目标。我的研究没有产生任何关于如何使用计算表达式的好例子。我发现的许多示例都停留在展示构建器的声明方式或稍后声明,但未能展示如何实际编写最终表达式。

所以,如果有人有时间发布一个真实的示例,它可以完成我上面的乱码代码试图做的事情,这将是最有启发性的,并填补了互联网上关于这方面的空白(至少对我而言) ) f# 编程的令人困惑的方面。

在我上面的代码示例中,我也看不到我首先从 builder monad 中得到了什么。与非单子实现相比,格式化程序代码看起来并不干净。

如果有人在答案帖子中将签名和类型添加到参数中,那就太好了;至少对我来说,与“让编译器查找类型”的风格相比,它更容易理解。

【问题讨论】:

  • 您对使用计算表达式的函数式解决方案感兴趣吗?我有一个我在几个项目中使用过的非常相似的东西,它非常简单易用。如果您愿意,我很乐意发布。
  • @JackP。这肯定会丰富讨论,并且非常受欢迎!虽然它不能帮助我理解 f# 中的一元编程是如何工作的,但它会很好地匹配手头的主题。

标签: f# code-generation computation-expression


【解决方案1】:

好的,正如我在 cmets 中提到的,这是一个我已经使用了一段时间并取得了成功的函数式解决方案,虽然它不是纯粹的-函数式的,它只是使用了一些简单的函数而不是计算表达式。

首先,代码:从我的 facio 存储库中获取 CodeGen.fs。如果您想了解我在实践中是如何使用这些功能的,请查看FSharpLex/Backend.Fslex.fsFSharpYacc/Backend.Fsyacc.fs

所以,以下是我这样实现代码生成的原因:

  • 我在IndentedTextWriter 模块中定义的函数非常轻量级并且(IMO)易于使用。如果您决定在自己的代码中使用我的函数,您可以放弃模块上的[&lt;RequireQualifiedAccess&gt;] 属性或将其更改为[&lt;AutoOpen&gt;] 以减少噪音。

  • 与其实现一堆代码来管理缩进级别并将缩进字符串发送到底层StringBuilder,我更喜欢使用System.CodeDom.Compiler.IndentedTextWriter,因为它会为您处理所有这些,它也是@的一个实例987654336@,因此您可以将其与fprintffprintfn 等函数一起使用。

    奖励:IndentedTextWriter 包含在 System.dll 中,因为您几乎肯定会引用它,所以您甚至不需要添加额外的引用来使用它!

  • IndentedTextWriter 只是包装了TextWriter 的另一个实例,因此您使用它编写的代码(例如,使用我在CodeGen.fs 中的函数)不会绑定到特定的“目标”。换句话说,您可以轻松地将其修改为写入 StringBuilder(使用StringWriter)、磁盘上的文件(使用StreamWriter)等等。

在你自己的代码中,你可以做这样的事情(只是给你一个想法):

let rec Block (formatters : Formatter list) (itw : IndentedTextWriter) =
    itw.WriteLine "{"
    IndentedTextWriter.indented itw <| fun itw ->
        formatters |> List.iter (fun fmtr -> fmtr itw)
    itw.WriteLine "}"

关于您的伪代码的另一个注意事项:因为您的格式化状态是可变的(就像我的代码中的 IndentedTextWriter 一样),实际上没有必要将它 out 传递给您的函数 - 也就是说,您通常只需要创建在这些状态由不可变对象/值表示时采用 返回状态值的函数。

奇怪的是,当传递一个可变写入器时(如我们这里的代码),您实际上需要“读取器”工作流或其某种变体。 ExtCoreExtCore.Control.Collections.Reader module 中包含用于列表、数组等的“读取器”式函数,您可以使用它来进一步简化代码。

【讨论】:

  • 感谢您的帖子!我之前尝试过制作一个非单子解析器,它具有可变状态,但我发现它与序列的行为不能很好地混合(我的输入是 char seq)。最后,当我的调试在意想不到的时刻遇到令人惊讶的断点时,我取消了这种方法,当然状态已经改变了。所以我想:我需要单子,因为 f# 控制流是不可预测的。这就是为什么这次是单子。希望做到!产生可预测的控制流。我想我会遵循你的方法,除非 f# Guru 在这里展示它“应该”如何完成:)
  • 解析器编写起来非常棘手,并且是不可变状态如何简化代码的一个很好的例子。我上面展示的代码生成也是 monadic——如果你愿意,你可以轻松地修改我的函数以在工作流中工作。以这种方式编写代码生成应该会给您一个非常可预测且易于调试的结果。
  • 几周过去了,重新访问这个尚未解决的问题,我觉得有必要补充:单子应该有助于开发迷你 dsls。这样一个 dsl 的主要目的可能是从符号中隐藏上下文(例如作者),使其看起来像一点 c++ 源代码,而不是 writer.WriteLine("...")。我仍然不知道如何解决这个问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多