【问题标题】:How can I improve the API of a pure function that returns state in Haskell?如何改进在 Haskell 中返回状态的纯函数的 API?
【发布时间】:2023-02-26 10:12:30
【问题描述】:

我正在尝试编写一个函数,它给定一些函数 f memoizes f,这样当调用 g = memoize f 后跟 g x 时,所有后续调用函数 g 和参数 x 只返回缓存的结果。

但是,我正在努力想出一个改进以下所需的显式状态传递的实现:

memoize :: Ord t => (t -> a) -> Map t a -> t -> Map t a
memoize f m a = case Map.lookup a m of
    Just _  -> m
    Nothing -> Map.insert a (f a) m

用一个人为的例子来展示它的用法:

main :: IO ()
main = do
  let memoPlusOne = memoize (+ 1) in
    let m = memoPlusOne Map.empty 1
      in let mm = memoPlusOne m 1
        in print mm

我知道有 other, better ways to memoize functions in Haskell,但我的问题更关心改进将状态传递给函数的一般模式,以避免任何状态突变,否则这些突变会像其他语言一样被封装,例如正如 Ocaml 中的这个例子:

let memo_rec f =
  let h = Hashtbl.create 16 in
  let rec g x =
    try Hashtbl.find h x
    with Not_found ->
      let y = f g x in
      (* update h in place *)
      Hashtbl.add h x y;
      y
  in
  g

【问题讨论】:

  • 在 OCaml 中,可变状态不是必需的。您只需要使用Map.Make 来创建一个地图仿函数并使用它来代替Hashtbl
  • 在 OCaml 中,我们可以随处使用副作用来改变数据,所以这不是问题。在 Haskell 中,副作用是被禁止的,所以它更复杂。必须要么利用惰性数据结构(不是严格的Map)并可能获得次优性能,要么使用unsafe函数颠覆系统,以便无论如何执行变异/副作用。由于从外部无法观察到副作用(记忆函数表现为纯函数),因此不会破坏引用透明性。您可以查看 Hoogle/Hackage 中的记忆库并研究它们的方法。
  • 我的建议是:不要重新发明轮子,也不要在严肃的代码中玩弄unsafe 东西。相反,采用现有的记忆库并使用它——这样您就不会冒造成严重破坏的风险。

标签: haskell state ocaml immutability


【解决方案1】:

我的问题更关心改进将状态传递给函数的一般模式,以避免任何状态突变,否则这些突变会像其他语言一样被封装

在 Haskell 中有很多方法可以用不可变状态做有趣的事情!我可以举一些例子,但我也觉得有义务指出 memoize 的最高效和用户友好的版本可能会在引擎盖下使用 unsafe,如果那是你想要的,你可能会更好使用现有的库而不是自己弄乱它。但是,如果您正在尝试,那就试试吧!

也就是说,在我们开始使用新技巧之前,让我们先看看您创建了什么。您当前代码的最大问题是类型已关闭。您有 memoize f m :: t -> Map t a,它甚至不会按预期产生预期的 f t 结果。毕竟,理论上 memoize 的最佳类型签名是 (t -> a) -> t -> a

您可以通过将 memoize 更改为以下内容来解决此问题:

memoize :: Ord t => (t -> a) -> Map t a -> t -> (Map t a, a)
memoize f m t = case Map.lookup t m of
    Just a  -> (m, a)
    Nothing -> let a = f t in (Map.insert t a m, a)

有了这个,您可以计算新的记忆状态,但也返回结果,这最终就是记忆的目的。这似乎是一个无关紧要的更改(您可能会问,您不能只是从 Map t a 中提取正确的 a 吗?)但是在探索如何处理状态时使用此类型签名很有用。


现在,回到您的问题:我们如何改进这种通过状态的一般模式?您可能会注意到您的函数接受一个状态并返回一个新状态,这就是 State monad 的全部内容。事实上,State 被简单地定义为:

newtype State s a = State {runState :: s -> (s, a)}

(在 transformers package 中,您可以在其中导入它,类型实际上有点不同,但它与此同构。)因此,您可以将 memoize 重写为 State monad,如下所示:

memoize :: Ord t => (t -> a) -> t -> State (Map t a) a
memoize f t = do
    m <- get
    case Map.lookup t m of
        Just a  -> pure a
        Nothing -> do
            let a = f t
            put (Map.insert t a m)
            pure a

恼人的是,你只能在 monad 中使用它。例如:

main :: IO ()
main = do
  let memoPlusOne = memoize (+ 1)
  flip runState Map.empty $ do
    res1 <- memoPlusOne 1
    res2 <- memoPlusOne 3
    print [res1, res2]

如果您在完成后对备忘录表感兴趣,也可以使用 evalState 而不是 runState


我们可以直接在函数中隐藏状态,而不是将状态隐藏在 monad 中。也就是说,我们不返回新状态,而是返回一个新函数:

newtype Memoized t a = Memo { runMemo :: t -> (Memoized t a, a) }

memoize :: Ord t => (t -> a) -> Memoized t a
memoize f = Memo $ go Map.empty
  where
    go m t = case Map.lookup t m of
      Just a  -> (Memo $ go m, a)
      Nothing -> let a = f t in (Memo $ go $ Map.insert t a m, a)

每次调用记忆函数时,这个技巧都会将状态捆绑在一个新的 Memoized 对象中。因此,只要每次创建时始终使用新的 Memoized 对象,就一定会始终保持记忆状态。考虑这个版本的main

main :: IO ()
main = do
  let memoPlusOne = memoize (+ 1)
  let (memoPlusOne', res1) = runMemo memoPlusOne 1
  let (memoPlusOne'', res2) = runMemo memoPlusOne' 3
  print [res1, res2]

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-25
    • 2012-01-02
    • 1970-01-01
    • 2013-02-01
    • 2012-02-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多