从C++到哈斯克尔类和状态

From C ++ to Haskell Classes and States

本文关键字:状态 哈斯克 C++      更新时间:2023-10-16

我必须转换这个C++代码

class A {
public: 
   int x_A;
    void setX_A (int newx) {
        x_A = newx;
    }
    void printX_A() {
       printf("x_A is %d", x_A);
    }
};
class B : public A {
public:
    int x_B;
    void setX_B (int newx) {
       x_B = newx;
    }
    void printX_B() {
       printf("x_B is %d", x_B);
    }
};
main() {
    A objA;
    B objB;
    objA.setX_A(2);
    objA.printX_A();
    objB.printX_A();
    objB.setX_B(5);
    objB.printX_B();
}

到 Haskell 代码中,并使用 State(或 StateT(Monad 模拟main()

到目前为止,我所做的是:

import Control.Monad.State
import Control.Monad.Identity
-- Fields For A
data FieldsA = FieldsA {x_A::Int} deriving (Show)
    -- A Class Constructor
constA :: Int -> FieldsA
constA = FieldsA
class A a where
    getX_A :: StateT a IO Int
    setX_A :: Int -> StateT a IO ()
    printX_A :: StateT a IO ()
instance A FieldsA where
    getX_A = get >>= return . x_A
    setX_A newx = do
        fa <- get
        put (fa { x_A = newx })
    printX_A = do
        fa <- get
        liftIO $ print fa
        return ()

data FieldsB = FieldsB{ fa::FieldsA, x_B::Int } deriving (Show)
constB :: Int -> Int -> FieldsB
constB int1 int2 = FieldsB {fa = constA int1, x_B = int2}
class A b => B b where
    getX_B :: StateT b IO Int
    setX_B :: Int -> StateT b IO ()
    printX_B :: StateT b IO ()
-- A Functions for Class B
instance A FieldsB where
    getX_A = do
      (FieldsB (FieldsA x_A) x_B) <- get
      return (x_A)
    setX_A newx = do
        (FieldsB (FieldsA x_A) x_B) <- get
        put (constB newx x_B)
    printX_A = do
        fb <- get
        liftIO $ print fb
        return ()
-- B specific Functions
instance B FieldsB where
    getX_B = get >>= return . x_B
    setX_B newx = do
        fb <- get
        put (fb { x_B = newx })
    printX_B = do
        fb <- get
        liftIO $ print fb
        return ()
test :: StateT FieldsA (StateT FieldsB IO ) ()
test = do
      x <- get
      setX_A 4
      printX_A
      --lift $ setX_A 99
      --lift $ setX_B 99
      --lift $ printX_A
      --lift $ printX_B
      --printX_A
      return ()
go = evalStateT (evalStateT test (constA 1)) (constB 2 3)
--go = runIdentity $ evalStateT (evalStateT test (constA 1)) (constA 1)

测试正在main() .

现在关于我遇到的问题:当我使用 lift 时,它可以正常工作,因为该功能变为 StateT FieldsB 型,但是当我尝试在没有 lift 的情况下使用setX_A时,会出现一个问题

*** Type           : StateT FieldsA IO ()
*** Does not match : StateT FieldsA (StateT FieldsB IO) ()

如果我将setX_A的类型更改为第二种,那么当我将其与 lift 一起使用时,它将不起作用(因为 B 类是从 A 派生的(。

首先,感谢您提供大量细节,它使您更容易理解您的问题!

现在,您在这里采用的方法可能并不理想。它为每个对象引入了一个新的StateT,这是导致您遇到的许多困难的原因,添加更多对象会使事情逐渐变得更糟。同样使问题复杂化的是,Haskell没有内置的子类型概念,并且使用类型类上下文模仿它将...工作,有点笨拙,不是最好的。

虽然我相信你意识到这是非常命令式的代码,并且直接将其翻译成Haskell有点愚蠢,但这就是任务,所以让我们谈谈更接近标准Haskell的方法。

命令式代码

状态单子风格:

暂时IO放在一边,要在纯代码中执行类似操作,典型的方法是:

  • 创建保存所有状态的数据类型
  • 使用getput"修改"状态

对于输出,您可以在 IO 周围使用 StateT,或者您可以在表示输出的状态数据中添加一个字段,保存 String 的列表,并在没有IO的情况下执行整个操作。

这是最接近当前方法的"正确"方法,大致是@Rotsor建议的。

IO 单子样式

上面仍然要求在函数外部预先指定所有可变变量,方法是在状态数据中定义它们。与其以这种方式处理事情,您还可以更直接地模仿原始代码,并在IO中使用真实的、诚实的可变状态。仅以A为例,您将得到如下所示的内容:

data FieldsA = FieldsA { x_A :: IORef Int}
constA :: Int -> IO FieldsA
constA x = do xRef <- newIORef x
              return $ FieldsA xRef
class A a where
    getX_A :: a -> IO Int
    setX_A :: a -> Int -> IO ()
    printX_A :: a -> IO ()
instance A FieldsA where
    getX_A = readIORef . x_A
    setX_A = writeIORef . x_A
    printX_A a = getX_A a >>= print

这在概念上更接近原文,并且符合@augustss在关于该问题的评论中所建议的。

略有变化是将对象保留为简单值,但使用IORef来保存当前版本。这两种方法之间的区别大致相当于,在 OOP 语言中,具有更改内部状态的 setter 方法的可变对象与具有可变引用它们的不可变对象。

对象

另一半困难在于 Haskell 中的继承建模。你使用的方法是最明显的方法,许多人跳到,但它有些有限。例如,您不能在任何需要超类型的上下文中真正使用对象;例如,如果一个函数有一个像(A a) => a -> a -> Bool这样的类型,没有简单的方法可以将其应用于A的两个不同的子类型。您必须将自己的强制转换实现到超类型。

这是一个替代翻译的草图,我认为在 Haskell 中使用它既更自然,又更准确地用于 OOP 风格。

首先,观察所有类方法如何将对象作为第一个参数。这表示 OOP 语言中隐含的"这个"或"自我"。我们可以通过将方法预先应用于对象的数据来保存步骤,以获取已经"绑定"到该对象的方法集合。然后我们可以将这些方法存储为数据类型:

data A = A { _getX_A :: IO Int
           , _setX_A :: Int -> IO ()
           , _printX_A :: IO ()
           }
data B = B { _parent_B :: A 
           , _getX_B :: IO Int
           , _setX_B :: Int -> IO ()
           , _printX_B :: IO ()
           }

我们将使用它们来提供对超类型的强制转换,而不是使用类型类来提供方法:

class CastA a where castA :: a -> A
class CastB b where castB :: b -> B
instance CastA A where castA = id
instance CastA B where castA = _parent_B
instance CastB B where castB = id

我们可以使用更高级的技巧来避免为每个伪 OOP "类"创建一个类型类,但我在这里保持简单。

请注意,我在上面的对象字段前面加上了下划线。那是因为这些是特定于类型的;现在我们可以为任何可以转换为我们需要的类型定义"真实"方法:

getX_A x = _getX_A $ castA x
setX_A x = _setX_A $ castA x
printX_A x = _printX_A $ castA x
getX_B x = _getX_B $ castB x 
setX_B x = _setX_B $ castB x
printX_B x = _printX_B $ castB x

为了构造新对象,我们将使用初始化内部数据的函数(相当于 OOP 语言中的私有成员(,并创建表示对象的类型:

newA x = do xRef <- newIORef x
            return $ A { _getX_A = readIORef xRef
                       , _setX_A = writeIORef xRef
                       , _printX_A = readIORef xRef >>= print
                       }
newB xA xB = do xRef <- newIORef xB
                parent <- newA xA
                return $ B { _parent_B = parent
                           , _getX_B = readIORef xRef
                           , _setX_B = writeIORef xRef
                           , _printX_B = readIORef xRef >>= print
                           }

请注意,newB调用newA并获取保存其成员函数的数据类型。它不能直接访问A的"私有"成员,但如果它愿意,它可以替换A的任何功能。

现在,我们可以以与您的原始方法几乎相同的方式使用这些方法,无论是在风格上还是在含义

上,例如:
test :: IO ()
test = do a <- newA 1
          b <- newB 2 3
          printX_A a
          printX_A b
          setX_A a 4
          printX_A a
          printX_B b

我认为您的问题是您没有一种很好的方法来指定您正在操作的对象。为了解决这个问题,我建议使用单独的程序状态,将两个对象都括起来:

data MainState = MainState { objA :: FieldsA, objB :: FieldsB }

现在你的主函数monad可以看起来像这样:

type Main t = StateT MainState IO t

并且,要选择您正在使用的对象,您可以使用如下内容:

withObjA :: StateT FieldsA IO t -> Main t
withObjB :: StateT FieldsB IO t -> Main t

用法如下所示:

test :: Main ()
test = do
    withObjA $ do
        setX_A 2
        printX_A
    withObjB $ do
        printX_A
        setX_B 5
        printX_B

更新:

以下是实现withObjAwithObjB的方法:

withPart :: Monad m => (whole -> part) -> (part -> whole -> whole) -> StateT part m t -> StateT whole m t
withPart getPart setPart action = do
    whole <- get
    (t, newPart) <- lift $ runStateT action (getPart whole)
    put (setPart newPart whole)
    return t
withObjA :: StateT FieldsA IO t -> Main t
withObjA = withPart objA (objA mainState -> mainState { objA = objA })
withObjB :: StateT FieldsB IO t -> Main t
withObjB = withPart objB (objB mainState -> mainState { objB = objB })

在这里,函数withPart actionpart上操作提升为在whole上运行的操作,使用getPart从整体中提取部分,setPart更新整体的一部分。如果有人告诉我库函数做类似的事情,我将不胜感激。 withObjAwithObjB是通过将它们各自的访问器函数传递给withPart来实现的。