【问题标题】:F# - Mutable context when applying sequence transformationF# - 应用序列转换时的可变上下文
【发布时间】:2013-08-23 04:29:55
【问题描述】:

为帖子的奇怪标题道歉,我不确定描述它的最佳方式是什么。

一般问题:

对Seq.map(或类似函数)做一个顺序应用,除了列表中的每一项外,还要传入一个“上下文”。每次迭代都可以修改这个“上下文”,更新后的版本应该传递到列表中的下一项。

具体问题:

我正在用 F# 创建一个编译器。我目前正在进行的步骤是将基于堆栈的 IL 转换为基于寄存器的 IL。我在想我可以“遍历”基于堆栈的 IL 并携带当前的“eval 堆栈”(类似于 .NET 的 eval 堆栈)。显然,每个堆栈 IL 操作码都会改变堆栈(例如:“添加”操作码将两个项目从堆栈中弹出并推送结果)。这个更新的堆栈将被传递到下一个操作码的发射周期。

请注意,我对函数式编程非常陌生(我是在一周前了解到的),来自 C# 背景,我的主要问题是“什么是‘函数式’方法来做到这一点?”

这是我对“功能性”方法的最佳猜测(伪代码)。我不喜欢“transformStackToRegisterIL”的元组返回值,如果我想保持不可变值的标准,是否需要它?另外,我担心过长的 IL 块会导致堆栈溢出,这是我的担忧吗?

let rec translate evalStack inputIl =
    match inputIl with
        | singleOpcode :: tail ->
            let (transformed, newEvalStack) = transformStackToRegisterIL evalStack singleOpcode
            transformed :: translate newEvalStack tail
        | [] -> []

编辑:List.scan 是我想要的内置函数吗? (看起来很相似,但并不完全正确......但它可能是正确的,我不确定)

【问题讨论】:

    标签: compiler-construction f# functional-programming


    【解决方案1】:

    我将尝试使用一个非常基本的示例来解释这一点,该示例在某种程度上受到您的问题的启发(但没有实现任何现实的东西)。

    所以,假设我们有 IL 指令 Push 将命名变量压入堆栈,Add 在堆栈中添加最后两个项目(为简单起见,假设它只是将结果打印到控制台)。目标是具有NopAdd 的注册表语言,它采用两个变量名称,添加它们(并将结果打印到控制台):

    type IL = 
      | Push of string
      | Add
    
    type Reg =
      | Add of string * string
      | Nop
    
    let input = [ IL.Push "a"; IL.Push "a"; IL.Push "b"; IL.Add; IL.Push "c"; IL.Add ]
    

    输入应转换为Reg.Add("b", "a")Reg.Add("c", "a") 以及一些Nops。转换函数采用当前堆栈和一条指令:

    let transform stack = function
      | IL.Push var -> Reg.Nop, var::stack
      | IL.Add -> Add(stack.Head, stack.Tail.Head), stack.Tail.Tail
    

    要转换整个列表,我们可以使用保持当前“状态”的List.fold。它调用具有当前状态和单个输入指令的提供函数,并且提供的函数必须返回一个新状态。这里,“状态”是堆栈,也是我们正在生成的寄存器机器指令列表:

    let endStack, regsReversed =
      input |> Seq.fold (fun (stack, regs) il ->
          // Transform current IL instruction, given current 'stack'
          let reg, newStack = transform stack il
          // Add new registry instruction to 'regs' and return new stack
          (newStack, reg::regs) ) ([], [])
    

    同样可以使用递归来完成。结构非常相似,只是我们将状态保留为参数并通过递归调用来更改它:

    let rec compile (stack, regs) = function
      | [] -> (stack, regs)
      | il::ils -> 
          // Transform current IL instruction, given current 'stack'
          let reg, newStack = transform stack il
          // Add new registry instruction to 'regs' and return new stack
          compile (newStack, reg::regs) ils
    
    let endStack, regs = compile ([], []) input
    

    现在我们可以检查堆栈最后是否为空并打印注册机指令(注意我们将它们附加到前面,因此我们需要将结果反转):

    if endStack <> [] then printfn "Stack is not empty!"
    regs |> List.rev
    

    正如 Jack 提到的 - 您还可以使用更高级的方法来处理此问题,例如计算表达式 (state)。我认为编写严肃的编译器实际上是一个使用它们有意义的地方,但如果你正在学习 F#,从折叠和递归等基本概念开始会更容易。

    【讨论】:

      【解决方案2】:

      传递一个“上下文”并改变它——你说的是state 工作流程;在那里,状态将是您的评估堆栈。

      如果您确实使用state 工作流程(我建议您这样做),您可以使用ExtCore 中的State.List.map 函数——它将一个列表映射到另一个列表,从一个元素传递上下文值处理列表时转到下一个。

      不用担心长 IL 块(即大的方法体)会使堆栈溢出——只有在调用堆栈非常深时才会担心堆栈溢出。

      【讨论】:

        【解决方案3】:

        您可以使用List.reduce 或自定义computation expression 来做到这一点(类似于异步的工作方式)。我可能会使用List.reduce,除非您要经常重复使用它,或者由于其他原因它没有修复List.reduce

        【讨论】:

        • 我不完全确定 List.reduce 是如何工作的——你能给出一个代码示例吗?似乎它的目标是更多的聚合/减少,而不是带有“上下文”的“地图”功能。
        • 啊,对。我的意思是List.fold。我有时会把这两个弄糊涂。
        猜你喜欢
        • 2018-08-19
        • 1970-01-01
        • 2015-08-09
        • 2011-01-09
        • 2023-02-08
        • 1970-01-01
        • 2010-09-28
        • 2015-08-31
        • 1970-01-01
        相关资源
        最近更新 更多