F# - 应用序列转换时的可变上下文

时间:2013-08-20 23:36:18

标签: compiler-construction f# functional-programming

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

一般问题:

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

具体问题:

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

请注意,我对函数式编程(我在一周前就已经了解它)非常陌生,来自C#背景,我的主要问题是“实现这一目的的'功能'方式是什么?”

这是我对这种“功能性”方法的最佳猜测(psudocode)。我不喜欢“transformStackToRegisterIL”的元组返回值,如果我想保持不可变值的标准,它是否需要?另外,我担心过长的IL块会出现堆栈溢出,这是我的一个有效问题吗?

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

编辑:List.scan是否是我想要的内置函数? (它似乎相似,但不完全正确......但它可能是正确的,我不确定)

3 个答案:

答案 0 :(得分:3)

我将尝试使用一个非常基本的例子来解释这一点,这个例子在某种程度上受到你的问题的驱动(但没有实现任何现实的)。

所以,我们假设我们有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来保持当前的“状态”。它使用当前状态和单个输入指令调用提供的函数,并且提供的函数必须返回新状态。这里,“state”是堆栈,但也是我们正在生成的寄存器机器指令列表:

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

正如杰克所说 - 你也可以使用更高级的处理方法,如计算表达式( state )。我认为编写严肃的编译器实际上是一个使用它们的地方,但如果你正在学习F#,那么从折叠和递归等基本概念开始就更容易了。

答案 1 :(得分:3)

传递“背景”并改变它 - 你在谈论state工作流程;在那里,状态将是你的评估堆栈。

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

不要担心使用长IL块(即大型方法体)溢出堆栈 - 一旦你有非常深的调用堆栈,堆栈溢出真的只是一个问题。

答案 2 :(得分:0)

您可以使用List.reduce或使用自定义computation expression执行此操作(类似于异步工作方式)。我可能会使用List.reduce,除非您经常重复使用它,或者由于其他原因而无法修复List.reduce