【问题标题】:Is there a way to elegantly represent this pattern in Haskell?有没有办法在 Haskell 中优雅地表示这种模式?
【发布时间】:2014-08-03 17:15:28
【问题描述】:

注意下面的纯函数,用命令式语言:

def foo(x,y):
    x = f(x) if a(x)
    if c(x): 
        x = g(x)
    else:
        x = h(x)
    x = f(x)
    y = f(y) if a(y)
    x = g(x) if b(y)
    return [x,y]

该函数表示您必须增量更新变量的样式。在大多数情况下可以避免这种情况,但在某些情况下这种模式是不可避免的——例如,为机器人编写烹饪程序,这本质上需要一系列步骤和决策。现在,假设我们试图在 Haskell 中表示 foo

foo x0 y0 =
    let x1 = if a x0 then f x0 else x0 in
    let x2 = if c x1 then g x1 else h x1 in
    let x3 = f x2 in
    let y1 = if a y0 then f y0 else y0 in
    let x4 = if b y1 then g x3 else x3 in
    [x4,y1]

该代码有效,但由于需要手动管理数字标签,它过于复杂且容易出错。请注意,在设置了x1 之后,x0 的值不应再次使用,但它仍然可以。如果您不小心使用了它,那将是一个未检测到的错误。

我已经设法使用 State monad 解决了这个问题:

fooSt x y = execState (do
    (x,y) <- get
    when (a x) (put (f x, y))
    (x,y) <- get
    if c x 
        then put (g x, y) 
        else put (h x, y)
    (x,y) <- get
    put (f x, y)
    (x,y) <- get
    when (a y) (put (x, f y))
    (x,y) <- get
    when (b y) (put (g x, x))) (x,y)

这样,标签跟踪的需求以及意外使用过时变量的风险就消失了。但是现在代码很冗长,更难理解,主要是因为(x,y) &lt;- get的重复。

那么:有什么更易读、更优雅、更安全的方式来表达这种模式

Full code for testing.

【问题讨论】:

  • 嗯,首先,您只需要一个 let 开头和一个 in 结尾。此外,有状态的计算可以用 state monad 来表达,尽管我不确定在这种情况下这是否有很大的改进;首先,它实际上并不比您的命令式示例长。
  • @Cubic 是真的,谢谢。问题更多在于数字标记,因为它令人困惑,容易出错。例如,x1 设置后,x0 不应该再次使用,但如果你犯了错误并这样做,这将是一个未检测到的错误。我试图通过 State monad 找到答案,但代码变得更加臃肿(尽管更正确)。我想我会尽快更新问题。
  • 我建议您停止强制式思考,而是以功能性方式思考。我同意适应新模式需要一些时间,但尝试将命令式思想转换为函数式语言并不是一个好方法。如果我知道您要达到的目标,我可以帮助您以更优雅的方式解决您的问题。不幸的是,你的 foo 函数并没有给我太多的想法。
  • 我是,但这一个必要的情况。具体来说,它是一种决策算法,用于决定 AI 在 TCG 游戏中应如何使用他的牌。就像烹饪一样,它自然会涉及一系列步骤和决策——我并没有做太多的事情。
  • 你绝对可以使用 state monad 的 modify 函数来清理它。

标签: haskell coding-style functional-programming


【解决方案1】:

你的目标

虽然命令式代码的直接转换通常会导致 ST monad 和 STRef,但让我们想想你真正想要做什么:

  1. 您希望有条件地操作值。
  2. 您想要返回该值。
  3. 您希望对操作步骤进行排序。

要求

现在这确实首先看起来像 ST monad。但是,如果我们遵循简单的 monad 法则,再加上 do 符号,我们会看到

do 
   x <- return $ if somePredicate x then g x
                                    else h x
   x <- return $ if someOtherPredicate x then a x
                                         else b x

正是你想要的。由于您只需要 monad 的最基本功能(return&gt;&gt;=),因此您可以使用最简单的:

Identity monad

foo x y = runIdentity $ do
    x <- return $ if a x then f x
                         else x
    x <- return $ if c x then g x
                         else h x
    x <- return $ f x 
    y <- return $ if a x then f y
                         else y
    x <- return $ if b y then g x
                         else y
    return (x,y)

请注意,您不能使用let x = if a x then f x else x,因为在这种情况下,x 两边都相同,而

x <- return $ if a x then f x 
                     else x

相同
(return $ if a x then (f x) else x) >>= \x -> ...

if 表达式中的 x 显然与生成的不一样,后者将用于右侧的 lambda。

助手

为了让这个更清楚,你可以添加像这样的助手

condM :: Monad m => Bool -> a -> a -> m a
condM p a b = return $ if p then a else b

获得更简洁的版本:

foo x y = runIdentity $ do
    x <- condM (a x) (f x) x
    x <- fmap f $ condM (c x) (g x) (h x)    
    y <- condM (a y) (f y) y
    x <- condM (b y) (g x) x
    return (x , y)

三元疯狂

在我们完成它的同时,让我们开始疯狂并引入一个三元运算符:

(?) :: Bool -> (a, a) -> a
b ? ie = if b then fst ie else snd ie

(??) :: Monad m => Bool -> (a, a) -> m a
(??) p = return . (?) p

(#) :: a -> a -> (a, a)
(#) = (,)

infixr 2 ??
infixr 2 #
infixr 2 ?

foo x y = runIdentity $ do
    x <- a x ?? f x # x
    x <- fmap f $ c x ?? g x # h x
    y <- a y ?? f y # y
    x <- b y ?? g x # x
    return (x , y)

但底线是,Identity monad 拥有您完成此任务所需的一切。

命令式或非命令式

人们可能会争论这种风格是否是必要的。这绝对是一系列动作。但是没有状态,除非你计算绑定变量。但是,一组let … in … 声明也给出了一个隐式序列:您希望第一个let 先绑定。

使用Identity 纯粹是功能性的

无论哪种方式,上面的代码都不会引入可变性。 x 不会被修改,而是有一个新的 xy 覆盖最后一个。如果您如上所述对do 表达式进行脱糖,这一点就会变得清晰:

foo x y = runIdentity $
      a x ?? f x # x   >>= \x ->
      c x ?? g x # h x >>= \x ->
      return (f x)     >>= \x ->
      a y ?? f y # y   >>= \y ->
      b y ?? g x # x   >>= \x ->
      return (x , y)

摆脱最简单的monad

但是,如果我们在左侧使用(?) 并删除returns,我们可以将(&gt;&gt;=) :: m a -&gt; (a -&gt; m b) -&gt; m b) 替换为a -&gt; (a -&gt; b) -&gt; b 类型的东西。这恰好是flip ($)。我们最终得到:

($>) :: a -> (a -> b) -> b
($>) = flip ($)     
infixr 0 $> -- same infix as ($)

foo x y = a x ? f x # x   $> \x ->
          c x ? g x # h x $> \x ->
          f x             $> \x ->
          a y ? f y # y   $> \y ->
          b y ? g x # x   $> \x ->
          (x, y)

这与上面脱糖的do 表达式非常相似。请注意,Identity 的任何用法都可以转换为这种样式,反之亦然。

【讨论】:

  • 接受您的回答,因为它显然是提供我所要求的内容。现在,这是否是一种好的风格还有待商榷。有一次,我们必须做所有事情。然后 Haskell 来了,向我们展示了大多数情况下有更好的选择。大多数,但不是全部。在我看来,个问题本质上是必不可少的,对于那些人来说,这是一个很好的解决方案。
  • 我要感谢其他所有人,我已经调查了这里的每一个答案,并从中学到了很多东西。有趣的是,有多少创造性的方法可以应用于这个问题。我希望这个帖子成为未来读者的一个很好的参考。
  • 关于命令式编程:有一个模因明确宣传 Haskell 是最好的命令式语言 :) stackoverflow.com/questions/6622524/…(我同意 Identity monad 在这种情况下似乎是最直接的解决方案)
  • 我只想补充一点,这并不是人们可能认为的强制解决方案。这本质上只是一堆组合在一起的 lambda :)
  • @is7s:确实。最后两个例子现在应该清楚了:D。
【解决方案2】:

你说的问题看起来像是 arrows 的一个不错的应用程序:

import Control.Arrow

if' :: (a -> Bool) -> (a -> a) -> (a -> a) -> a -> a
if' p f g x = if p x then f x else g x

foo2 :: (Int,Int) -> (Int,Int)
foo2 = first (if' c g h . if' a f id) >>>
       first f >>>
       second (if' a f id) >>>
       (\(x,y) -> (if b y then g x else x , y))

特别是,first 将函数 a -&gt; b 提升为 (a,c) -&gt; (b,c),这更符合习惯。

编辑:if' 允许电梯

import Control.Applicative (liftA3)

-- a functional if for lifting
if'' b x y = if b then x else y

if' :: (a -> Bool) -> (a -> a) -> (a -> a) -> a -> a
if' = liftA3 if''

【讨论】:

    【解决方案3】:

    我可能会这样做:

    foo x y = ( x', y' )
      where x' = bgf y' . cgh . af $ x
            y' = af y
    
    af z    = (if a z then f else id) z
    cgh z   = (if c z then g else h) z
    bg y x  = (if b y then g else id) x
    

    对于更复杂的事情,您可能需要考虑使用镜头:

    whenM :: Monad m => m Bool -> m () -> m ()
    whenM c a = c >>= \res -> when res a
    
    ifM :: Monad m => m Bool -> m a -> m a -> m a
    ifM mb ml mr = mb >>= \b -> if b then ml else mr
    
    foo :: Int -> Int -> (Int, Int)
    foo = curry . execState $ do
      whenM (uses _1 a) $ 
        _1 %= f
    
      ifM (uses _1 c)
        (_1 %= g)
        (_1 %= h)
    
      _1 %= f
    
      whenM (uses _2 a) $ 
        _2 %= f
    
      whenM (uses _2 b) $ do
        _1 %= g
    

    没有什么能阻止您使用更具描述性的变量名称:

    foo :: Int -> Int -> (Int, Int)
    foo = curry . execState $ do
      let x :: Lens (a, c) (b, c) a b
          x = _1
          y :: Lens (c, a) (c, b) a b
          y = _2
    
      whenM (uses x a) $ 
        x %= f
    
      ifM (uses x c)
        (x %= g)
        (x %= h)
    
      x %= f
    
      whenM (uses y a) $ 
        y %= f
    
      whenM (uses y b) $ do
        x %= g
    

    【讨论】:

    • 当你努力并设法找到我的函数的一个相当漂亮和优雅的版本时,我正在支持你!但这正是我在编写如此复杂的函数时试图避免的那种答案。您所做的对这种模式的实际用例没有用处,例如我提到的“机器人烹饪程序”案例。
    【解决方案4】:

    这是ST(状态转换器)库的工作。

    ST提供:

    • 有状态计算,采用 ST 类型的形式。这些看起来像 ST s a 用于导致类型为 a 的值的计算,并且可以使用 runST 运行以获得纯 a 值。
    • 一流的可变引用,采用 STRef 类型的形式。 newSTRef a 操作创建一个新的STRef s a 引用,其初始值为a,可以用readSTRef ref 读取并用writeSTRef ref a 写入。单个 ST 计算可以在内部使用任意数量的 STRef 引用。

    这些让您可以表达与命令式示例相同的可变变量功能。

    要使用ST和STRef,我们需要导入:

    {-# LANGUAGE NoMonomorphismRestriction #-}
    import Control.Monad.ST.Safe
    import Data.STRef
    

    我们可以定义以下助手来匹配 Python 风格的 foo 示例使用的命令式操作,而不是到处使用低级别的 readSTRefwriteSTRef

    -- STRef assignment.
    (=:) :: STRef s a -> ST s a -> ST s ()
    ref =: x  =  writeSTRef ref =<< x
    
    -- STRef function application.
    ($:) :: (a -> b) -> STRef s a -> ST s b
    f $: ref  =  f `fmap` readSTRef ref
    
    -- Postfix guard syntax.
    if_ :: Monad m => m () -> m Bool -> m ()
    action `if_` guard  =  act' =<< guard
        where act' b = if b then action
                            else return ()
    

    这让我们可以写:

    • ref =: x 将 ST 计算 x 的值分配给 STRef ref
    • (f $: ref) 将纯函数 f 应用于 STRef ref
    • 仅当 guard 的结果为 True 时,action `if_` guard 才会执行 action

    有了这些助手,我们可以忠实地将foo 的原始命令式定义翻译成Haskell:

    a = (< 10)
    b = even
    c = odd
    f x = x + 3
    g x = x * 2
    h x = x - 1
    f3 x = x + 2
    
    -- A stateful computation that takes two integer STRefs and result in a final [x,y].
    fooST :: Integral n => STRef s n -> STRef s n -> ST s [n]
    fooST x y = do
        x =: (f $: x) `if_` (a $: x)
    
        x' <- readSTRef x
        if c x' then
            x =: (g $: x)
        else
            x =: (h $: x)
    
        x =: (f $: x)
        y =: (f $: y) `if_` (a $: y)
        x =: (g $: x) `if_` (b $: y)
    
        sequence [readSTRef x, readSTRef y]
    
    -- Pure wrapper: simply call fooST with two fresh references, and run it.
    foo :: Integral n => n -> n -> [n]
    foo x y = runST $ do
        x' <- newSTRef x
        y' <- newSTRef y
        fooST x' y'
    
    -- This will print "[9,3]".
    main = print (foo 0 0)
    

    注意事项:

    • 虽然在翻译 foo 之前我们首先必须定义一些语法助手(=:$:if_),但这演示了如何使用 ST 和 STRef 作为基础来发展自己的小命令式语言这直接适合手头的问题。
    • 撇开语法不谈,这与原始命令式定义的结构完全匹配,没有任何容易出错的重组。对原始示例的任何细微更改都可以直接镜像到 Haskell。 (在 Haskell 代码中添加临时 x' &lt;- readSTRef x 绑定只是为了将其与本机 if/else 语法一起使用:如果需要,可以将其替换为适当的基于 ST 的 if/else 构造。)李>
    • 上面的代码演示了为同一计算提供纯接口和有状态接口:纯调用者可以使用foo而不知道它在内部使用可变状态,而ST调用者可以直接使用fooST(例如提供它与现有的 STRef 一起修改)。

    【讨论】:

      【解决方案5】:

      @Sibi 在他的评论中说得最好:

      我建议你停止强制式思考,而是以功能性的方式思考。我同意适应新模式需要一些时间,但尝试将命令式思想转化为函数式语言并不是一个好方法。

      实际上,您的let 链可以是一个很好的起点:

      foo x0 y0 =
          let x1 = if a x0 then f x0 else x0 in
          let x2 = if c x1 then g x1 else h x1 in
          let x3 = f x2 in
          let y1 = if a y0 then f y0 else y0 in
          let x4 = if b y1 then g x3 else x3 in
          [x4,y1]
      

      但我建议使用单个 let 并为中间阶段提供描述性名称。

      不幸的是,在这个示例中,我不知道各种 x 和 y 的作用,因此我无法建议有意义的名称。在实际代码中,您会使用 x_normalizedx_translated 等名称,而不是 x1x2,来描述这些值的真正含义。

      事实上,在letwhere 中你并没有真正的变量:它们只是你给中间结果的简写名称,以便于组成最终表达式(@987654330 之后的那个@ 或在where 之前。)

      这就是下面x_barx_baz 背后的精神。考虑到代码的上下文,尝试提出具有合理描述性的名称。

      foo x y =
          let x_bar   = if a x then f x else x
              x_baz   = f if c x_bar then g x_bar else h x_bar
              y_bar   = if a y then f y else y
              x_there = if b y_bar then g x_baz else x_baz
          in  [x_there, y_bar]
      

      然后您可以开始识别隐藏在命令式代码中的模式。例如,x_bary_bar 基本上是相同的转换,分别应用于xy:这就是为什么在这个无意义的示例中它们具有相同的后缀“_bar”;那么你的x2 可能不需要中间名,因为你可以将f 应用于整个“if c then g else h”的结果。

      继续进行模式识别,您应该将应用于变量的转换分解为子 lambda(或任何您称之为在 where 子句中定义的辅助函数)。

      同样,我不知道原始代码做了什么,所以我无法为辅助函数建议有意义的名称。在实际应用中,f_if_a 将被称为 normalize_if_neededthaw_if_frozenmow_if_overgrown... 你明白了:

      foo x y =
          let x_bar   = f_if_a x
              y_bar   = f_if_a y
              x_baz   = f (g_if_c_else_h x_bar)
              x_there = g_if_b x_baz y_bar
          in  [x_there, y_bar]
      where
          f_if_a x
              | a x       = f x
              | otherwise = x
          g_if_c_else_h x
              | c x       = g x
              | otherwise = h x
          g_if_b x y
              | b y       = g x
              | otherwise = x
      

      不要忽视这个命名业务。

      Haskell 和其他纯函数式语言的全部意义在于表达没有赋值运算符的算法,意味着可以修改现有变量值的工具。

      你在函数定义中给事物起的名字,无论是作为参数引入的let,还是where,在整个定义中只能引用一个值(或辅助函数),这样你的代码就可以更容易推理和证明是正确的。

      如果你不给它们起有意义的名字(相反地给你的代码一个有意义的结构),那么你就错过了 Haskell 的全部目的。

      (恕我直言,到目前为止,引用 monads 和其他恶作剧的其他答案都是错误的。)

      【讨论】:

      • 但我猜你不明白这一点。想象一下,“x”代表一个厨房,函数代表一个使用该厨房的机器人。所以,你会得到类似的东西:kitchen = move_spoons_to_table(kitchen); if (there_are_eggs_on_fridge(kitchen)) put_sugar_on_table(kitchen); 等等。在这种情况下,中间步骤实际上没有任何有意义的名称。这只是厨房!而且,如果您真的为流程的每个快照使用描述性名称,您将得到一些非常奇怪的代码,其中包含诸如kitchenStateAfterBakingCakeButUsingAlternativeSweetener 之类的可憎之物。
      • 所以,重点是:算法本质上是命令式的。它确实描述了一个命令式操作。与 Haskell 的交易是大多数语言使用命令式风格来表达一切,即使我们的大多数程序本质上不是命令式的。但是有 事物本质上是必不可少的,并且也需要具有可表示性!话虽如此,我非常感谢您的回答和想法,并希望看到您对这些问题的答复。考虑到我所说的,您是否认为例如 Zeta 的回答是一个很好的方法 - 或者您仍然保持您的立场?谢谢!
      • 另外,很抱歉这么长的回复您的回答。希望我不是这里的吸血鬼!
      • @Viclib 是的,Zeta 的回答是在 Haskell 中表示一系列必要步骤的好方法。 do&lt;- 通过将每个 &lt;- 和之后的所有行包含在一个新的 lambda 中来工作,因此每个新的 x 实际上是一个新的函数参数,它“遮蔽”了前面(外部)的同名。您可以通过自己嵌套 lambdas 来实现相同的目的,而无需使用 monad,除了 do 为您提供了更好的语法。我不知道在不引入 monad 的情况下嵌套 lambda 是否有好的语法。
      • @Viclib 我仍然坚持认为,对于您可能想要使用它的大多数目的,最好以函数方式重写算法。例如,如果 x 和 y 是坐标,最好为它们所代表的点命名。一般来说,当您可以推断变量的“所有状态空间”(域)时,您也可以为特定点命名。如果您不能或不愿对整个域进行推理,则可以使用命令式步骤。
      【解决方案6】:

      我总是更喜欢分层状态转换器而不是在元组上使用单个状态:它通过让您“专注”于特定层(在我们的例子中表示 xy 变量)肯定会整理东西:

      import Control.Monad.Trans.Class
      import Control.Monad.Trans.State
      
      foo :: x -> y -> (x, y)
      foo x y = 
        (flip runState) y $ (flip execStateT) x $ do
          get >>= \v -> when (a v) (put (f v))
          get >>= \v -> put ((if c v then g else h) v)
          modify f
          lift $ get >>= \v -> when (a v) (put (f v))
          lift get >>= \v -> when (b v) (modify g)
      

      The lift function 让我们专注于内部状态层,即y

      【讨论】:

      • 不太同意。变形金刚很棒,但是你添加的越多,所有东西都会变得越复杂......对于focusing,显而易见的使用是lenses,正如rampion所证明的那样,在单一状态实体。
      猜你喜欢
      • 1970-01-01
      • 2016-03-25
      • 2023-01-23
      • 1970-01-01
      • 2012-04-14
      • 1970-01-01
      • 1970-01-01
      • 2017-03-31
      • 1970-01-01
      相关资源
      最近更新 更多