使用命令式语言来理解下面的纯函数:
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
的重复。
所以:表达这种模式的更可读,更优雅,更安全的方式?
答案 0 :(得分:29)
虽然命令式代码的直接转换通常会导致ST
monad和STRef
,但让我们考虑一下你真正想做的事情:
现在这确实看起来像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
不会被修改,而是会有一个新的y
或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)
表达式,那就明白了:
(?)
但是,如果我们在左侧使用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 s a
的值的计算,它们看起来像a
,并且可以与runST
一起运行以获得纯a
值。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
示例使用的命令式操作,而不是在整个地方使用低级writeSTRef
和foo
:
-- 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用作成长你自己的小命令语言的基础,直接适合手头的问题。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_normalized
,x_translated
等名称,而不是x1
和x2
来描述这些值的真实含义。
事实上,在let
或where
中,您确实没有变量:它们只是您为中间结果提供的简写名称,以便轻松编写最终表达式(in
之后或where
之前的那个。)
这是下面x_bar
和x_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_bar
和y_bar
基本上是相同的转换,分别应用于x
和y
:这就是为什么它们具有相同的后缀&#34; _bar&#34;在这个荒谬的例子中;那么你的x2
可能不需要中间名,因为你只需将f
应用于整个&#34的结果;如果是c则g,否则h&#34;。
继续进行模式识别,你应该将你应用于变量的变换分解为子lambda(或者你称之为where
子句中定义的辅助函数。)
同样,我不知道原始代码的作用,因此我无法为辅助功能建议有意义的名称。在实际应用中,f_if_a
将被称为normalize_if_needed
或thaw_if_frozen
或mow_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)
我总是喜欢将状态变换器分层为使用单个状态而不是元组:它通过让你“专注”来解决问题。在特定图层上(在我们的案例中代表x
和y
变量):
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
。