有没有办法在Haskell中优雅地表示这种模式?

时间:2014-08-03 17:15:29

标签: haskell coding-style functional-programming

使用命令式语言来理解下面的纯函数:

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) <- get的重复。

所以:表达这种模式的更可读,更优雅,更安全的方式

Full code for testing.

6 个答案:

答案 0 :(得分:29)

你的目标

虽然命令式代码的直接转换通常会导致ST monad和STRef,但让我们考虑一下你真正想做的事情:

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

    现在这确实看起来像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>>=)的最基本功能,因此您可以使用最简单的功能:

    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 -> ...
    

    并且x表达式中的if显然与生成的condM :: Monad m => Bool -> a -> a -> m a condM p a b = return $ if p then a else b 不同,后者将在右侧的lambda中使用。

    助手

    为了使这一点更清晰,您可以添加帮助器,如

    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

    但最重要的是,let … in … monad拥有此任务所需的一切。

    势在必行或非命令性

    有人可能会争论这种风格是否必要。这绝对是一系列行动。但除非你计算绑定变量,否则没有状态。但是,然后一包let声明也会给出一个隐式序列:您希望第一个Identity首先绑定。

    使用x纯粹是功能

    无论哪种方式,上面的代码都不会引入可变性。 x不会被修改,而是会有一个新的ydo影响最后一个。如果你如上所述去掉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

    但是,如果我们在左侧使用return并删除(>>=) :: m a -> (a -> m b) -> m b),我们可以将a -> (a -> b) -> 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

    这与上面的desugared Identity表达非常相似。请注意,{{1}}的任何用法都可以转换为此样式,反之亦然。

答案 1 :(得分:18)

您声明的问题看起来像是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 -> b提升为(a,c) -> (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''

答案 2 :(得分:11)

我可能会这样做:

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

答案 3 :(得分:9)

这是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样式readSTRef示例使用的命令式操作,而不是在整个地方使用低级writeSTReffoo

-- 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
  • action `if_` guard仅在action结果为True时执行guard

有了这些助手,我们可以忠实地将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)

注意事项:

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

答案 4 :(得分:6)

@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中,您确实没有变量:它们只是您为中间结果提供的简写名称,以便轻松编写最终表达式(in之后或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:这就是为什么它们具有相同的后缀&#34; _bar&#34;在这个荒谬的例子中;那么你的x2可能不需要中间名,因为你只需将f应用于整个&#34的结果;如果是c则g,否则h&#34;。

继续进行模式识别,你应该将你应用于变量的变换分解为子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的整个目的。

(恕我直言,到目前为止,其他答案,引用单子和其他恶作剧,正在咆哮错误的树。)

答案 5 :(得分:3)

我总是喜欢将状态变换器分层为使用单个状态而不是元组:它通过让你“专注”来解决问题。在特定图层上(在我们的案例中代表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