【问题标题】:Out-of-order list iteration in HaskellHaskell中的乱序列表迭代
【发布时间】:2021-08-06 07:22:56
【问题描述】:

我正在 Haskell 中实现一个(玩具)堆栈机器。我定义了一个步进函数step :: State -> Instruction -> State,它将给定指令的结果应用于给定状态并返回机器的结果状态。显然,我想要一个函数,run :: State -> Program -> State(其中Program :: [Instruction]),它基本上根据需要多次调用step,以便执行给定的输入程序。

我最初的天真的解决方案是foldl,就像这样:

run :: State -> Program -> State
run st prog = foldl (step) st prog

显然,这不支持跳转,这会修改我需要在列表中的位置。这个实现所做的就是从左到右遍历程序。对于其他上下文,程序的状态 State 如下:

data State = State {
    pc :: Word,
    reg :: Word,
    stack :: [Word],
    memory :: [Word]
}
    deriving (Show, Eq)

说明如下:

data Opcode = Add | Sub | Mul | Div | Mod | Jump | Push | Pop | Load | Store | Set | Call | Ret | Pos | Dup | Swap | Halt | Nop
    deriving (Enum, Show, Eq)

data Instruction = Instruction {
    opcode :: Opcode,
    arg :: Maybe Word
}
    deriving (Show, Eq)

如何以任意顺序(当然也可能永远)遍历列表,以便支持跳转

【问题讨论】:

  • 附带说明,您可能不应该将其称为 State,因为 Haskell 的标准库中已经存在 State 类型。
  • 这似乎是使用显式递归而不是依赖于foldl 之类的东西的好时机。如果程序不应该停止,则使用step 的结果进行递归,否则返回完成状态。由于链表的性质,您可能会发现跳转非常缓慢,因此这可能是look into arrays 的好时机。
  • 你有一个pc 但你正在使用折叠——这似乎有点不对劲,不是吗?如果你想使用一台电脑,那么你可能想要一个指令向量之类的东西,你可以索引到而不是一个列表。如果您更愿意使用该列表,请将 PC 完全报废并将程序存储在 State 中。这是诀窍:存储当前执行(尚未运行的指令列表)和“整个程序”。然后,处理Jump 成为可能。
  • @DDub 这是问题的症结所在,是的。我需要显式递归来启用“跳跃”本身,我只是在制定它时遇到了麻烦。值得一提的是,我有一个等效的 Rust 实现,但它使用可变状态来跟踪程序代码中的当前位置。
  • @Aplet123 感谢State 的警告——我假设Haskell 用它的模块系统管理得很好?不然我就改名字了。

标签: list haskell functional-programming iteration fold


【解决方案1】:

使用数组可能是一种不错的方法,但还有一种方法可以通过完全取消程序计数器来逐步执行程序,这更像是折叠。

程序计数器只是一个指向程序中您接下来要运行的指令的指针。因此,我们可以不使用程序计数器,而是将要运行的指令直接置于状态。考虑State 的这种替代实现:

data State = State
  { reg     :: Word
  , stack   :: [Word]
  , memory  :: [Word]
  , program :: Program
  } deriving (Show, Eq)

现在我们必须重新考虑step。特别是,每次我们step 时,我们都不会增加(或任意修改)pc,而是更改要运行的程序。此外,我们不需要将指令作为参数,因为State 已经知道将要运行什么。因此,我们有类似的东西:

step :: State -> State
step st = case program st of
  []          -> st
  HALT : _    -> st {program = []}
  ADD  : rest -> st {program = rest, ...} -- Do whatever you do for add too
  ...

但是,我们要为JUMP 做什么?当程序在我们运行时消失在我们身上时,我们如何跳转到程序中的任意位置?一种选择是另外跟踪原始程序。我们可以将它作为另一个字段放在State 中,但为了多样化,我将把它作为附加参数传递给step,如下所示:

step :: Program -> State -> State
step originalProgram st = case program st of
  []         -> st
  HALT : _   -> st {program = []}
  JUMP n : _ -> st {program = drop n originalProgram}
  ...

(请注意,这里我假设您的 JUMP 是绝对的,而不是相对的。如果您有相对的跳转,那么您不需要跟踪原始程序,而是需要跟踪“已经执行”程序的一部分,可能是一个反向顺序的指令列表。也就是说,每次执行指令时,都会将其从 program 列表中弹出并推送到 executed 列表中。当你点击 JUMP回到 n 指令,你只需将 n 从已执行的指令中弹出并将它们推回程序列表。)

现在剩下的就是运行整个程序:

run :: State -> Program -> State
run startState originalProgram = go (startState {program = originalProgram})
  where
    go st = case program st of
      [] -> st
      _  -> go $ step originalProgram st

就性能而言,这肯定比使用数组更糟糕;大多数步骤会很快,但 JUMPS 可能需要更长的时间。另一方面,您可以通过这种方式运行无限长的程序,而且您不必担心超出范围的索引。

【讨论】:

  • 我没想过以这种方式重新设计它——这非常优雅。
【解决方案2】:

我猜你的step 函数需要报告是否该停止。例如,假设您将其类型修改为step :: State -> Instruction -> Maybe State。然后你可以通过发送到run 来实现它:

run :: State -> Program -> State
run state prog = case step state (prog !! fromIntegral (pc state)) of
    Nothing -> state
    Just state' -> run state' prog

(您可以通过将pc 设置为Int 而不是Word 来避免fromIntegral。)

请注意 (!!) 是 O(2n)* (在 cmets 中与我战斗 ;-)。您应该考虑从[Instruction] 切换到Array Word Instruction,以便可以改用(!),即O(n)。

* 好的,准确地说,(!!) 在技术上是 O(1),因为Int 有一个固定的大小——但是,哦,那个常数因子!因此,假设(!!)Integer 的适当概括是 O(2n)。类似的问题也适用于(!)

【讨论】:

  • 我猜(+) @Integer也是O(n),那么:-P
  • @chi 是的!现在,如果你知道(*) 的最佳界限是什么,请告诉我...
  • 我猜它是 O(nlog nlog(log n)),“但是它的证明太长了,不适合这个评论”™。
  • 将此标记为答案,因为它直接解决了原始问题。
猜你喜欢
  • 2011-01-15
  • 1970-01-01
  • 2013-12-10
  • 1970-01-01
  • 1970-01-01
  • 2023-03-23
  • 2018-12-14
  • 1970-01-01
相关资源
最近更新 更多