状态设计模式的功能等效替代方案

14

状态设计模式在函数式编程中的等效方式是什么?或者更具体地说,这个维基百科的例子如何转换为函数式编程?

4个回答

9
这个模式是 State monad 的一个例子,它是一种计算环境,可以增强代码的状态。
以下是 Haskell 中的实现。
一些辅助函数:
import Control.Monad.Trans.State
import Control.Monad.IO.Class
import Data.Char

程序的两种操作模式
data Mode = A | B

使用此模式的有状态计算类型,附带计数器。
type StateM a = StateT (Int, Mode) IO a

write函数是StateM上下文中的一个函数,它的行为根据有状态模式而改变:

writeName :: String -> StateM ()
writeName s = do
    (n,mode) <- get
    case mode of
        A -> do liftIO (putStrLn (map toLower s))
                put (0,B)
        B -> do let n' = n + 1
                liftIO (putStrLn (map toUpper s))
                if n' > 1 then put (n', A)
                          else put (n', B)

运行程序,在状态 A 中最初启动一个有状态的计算

main = flip runStateT (0, A) $ do
    writeName "Monday"
    writeName "Tuesday"
    writeName "Wednesday"
    writeName "Thursday"
    writeName "Saturday"
    writeName "Sunday"

从上面的代码中,main函数的输出为:

monday
TUESDAY
WEDNESDAY
thursday
SATURDAY
SUNDAY

请注意,这是一个纯函数解决方案。在此程序中没有可变或破坏性的更新。相反,状态单子通过计算中线程所需的模式。

1
我不认为这是一个忠实的编码:在这里,我们必须在一个地方声明所有模式(data Mode),而在维基百科的例子中,声明可以按模块组合。 - luqui
这是ML风格语言中封闭数据类型的标准翻译。显然,我们可以通过函数或类型类来使用开放数据类型,但我认为这对于这个问题不相关。 - Don Stewart
1
我很好奇“标准翻译”是指什么,以及你所说的“标准”是什么意思。 - luqui
3
我认为惯用的方法是将具有多个变量的对象翻译成代数数据类型。 - Don Stewart

5

一种编码:

import Data.Char (toUpper, toLower)

newtype State = State { unState :: String -> IO State }

stateA :: State
stateA = State $ \name -> do
    putStrLn (map toLower name)
    return stateB

stateB :: State
stateB = go 2
    where
    go 0 = stateA
    go n = State $ \name -> do
               putStrLn (map toUpper name)
               return $ go (n-1)

不要被IO所迷惑,这只是该模式的纯翻译(我们没有使用IORef来存储状态或任何其他东西)。扩展newtype,我们可以看到这种类型的含义:

State = String -> IO (String -> IO (String -> IO (String -> ...

它接受一个字符串,进行一些I/O操作,并要求输入另一个字符串等。

这是我最喜欢的OO抽象类模式编码方式:抽象类 -> 类型,子类 -> 该类型的元素。

newtype State声明取代了抽象writeName声明及其签名。我们不需要将StateContext传递给它来分配新状态,而是让它返回新状态。将返回值嵌入IO中表示新状态允许依赖于I/O。由于在这个例子中技术上并不必要,因此我们可以使用更严格的类型。

newtype State = State { unState :: String -> (State, IO ()) }

我们可以使用一种方法来表达这个计算,但状态序列是固定的,不能依赖于输入。但让我们坚持原始的、更宽松的类型。

至于“测试客户端”:

runState :: State -> [String] -> IO ()
runState s [] = return ()
runState s (x:xs) = do
    s' <- unState s x
    runState s' xs

testClientState :: IO ()
testClientState = runState stateA
                   [ "Monday"
                   , "Tuesday"
                   , "Wednesday"
                   , "Thursday"
                   , "Saturday"
                   , "Sunday" ]

请捕获计数器切换语义。 - Don Stewart
说实话,我认为这种编码风格只适用于真正的面向对象习惯类,封装抽象行为。在所谓的面向对象代码中,令人沮丧的是,“类”更接近于Don使用的风格的笨拙翻译,因此从一开始就应该将它们转回到它们本来想要的样子... - C. A. McCann

1
也许可以使用一个带有自定义修饰符和访问器的 State Monad?

同意,对我来说这看起来像是State单子的一个特定用法。 - Don Stewart

-3

我认为在纯函数式编程中没有状态模式的纯函数式等效方法。因为纯函数式编程没有状态和时间的概念。状态模式本质上是关于状态和时间的。但我认为非纯函数式等效方法是存在的,它就是无限惰性求值流。你可以用C# yield来实现它。


1
FP确实有状态。从未听说过单子?甚至有一种称为函数响应式编程的东西来捕获时变状态。 - fuz
1
我认为"has"不是连接"函数式编程"和"状态"的正确词语。我更喜欢把它看作是"模型"——我们可以通过构建关于状态的函数式模型来进行有状态计算。 - luqui
1
@Todd 单子是完全纯净的。你可以在其中携带任何类型的状态。还有STM,它提供了可修改本地变量的概念。即使是STM也是纯净的,因为你可以证明,一个使用指定方式的STM函数是引用透明的。状态可能不是最流行的习惯用语。 - fuz
1
@FUZxxl,我认为你想到的是ST而不是STM - luqui
1
@luqui:我认为“has”实际上是一个可行的词,大致意思是“可供使用和操作”。因此,纯语言具有状态概念,而不纯语言仅具有内在状态。这就是Haskell拥有比任何命令式语言更多用于操作命令式计算的工具的看似矛盾之处。 - C. A. McCann
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接