从C++到Haskell类和状态

epi*_*ose 13 c++ monads haskell

我必须转换这个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();
}
Run Code Online (Sandbox Code Playgroud)

进入Haskell代码,并main()使用State(或StateT)Monad进行模拟.

到目前为止我所做的是:

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)
Run Code Online (Sandbox Code Playgroud)

测试存在main().

现在关于我StateT FieldsB遇到的问题:当我使用电梯时,它工作正常,因为功能变得类型,但是当我尝试使用setX_A没有电梯时出现问题

*** Type           : StateT FieldsA IO ()
*** Does not match : StateT FieldsA (StateT FieldsB IO) ()
Run Code Online (Sandbox Code Playgroud)

如果我将类型更改为setX_A第二个,那么当我使用它时它将无法工作(因为B类派生自A).

C. *_*ann 13

首先,感谢您提供的详细信息,让您更容易理解您的问题!

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

虽然我确定你意识到这是非常命令式的代码并且直接将它转换为Haskell有点愚蠢,这就是赋值,所以让我们谈谈如何做到这一点,更接近标准的Haskell.

势在必行的代码

State Monad风格:

设置IO放在一边,做这样的事情在纯代码典型的做法是这样的:

  • 创建一个包含所有状态的数据类型
  • 使用get和"修改"状态put

对于输出,你可以使用StateTaround IO,或者你可以在表示输出的状态数据中添加一个字段,保存一个Strings 的列表,并且不做任何事情IO.

这最接近于采用当前方法的"正确"方式,大致与@Rotsor建议的方式相同.

IO Monad风格

上述内容仍然要求通过在状态数据中定义所有可变变量,在函数外部预先指定它们.你可以更直接地模仿原始代码,而不是以这种方式处理事情,并使用真实的,诚实的上帝可变状态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
Run Code Online (Sandbox Code Playgroud)

这在概念上更接近于原始版本,并且与@augustss在该问题的评论中所建议的一致.

稍有不同的是将对象保持为简单值,但使用a IORef来保存当前版本.两种方法之间的差异大致等同于在OOP语言中,具有setter方法的可变对象,这些方法通过对它们的可变引用来更改内部状态与不可变对象.

对象

另一半困难在于在Haskell中建模继承.你正在使用的方法是许多人跳过的最明显的方法,但它有点受限.例如,在预期超类型的任何上下文中,您无法真正使用对象; 例如,如果函数具有类似的类型(A a) => a -> a -> Bool,则没有简单的方法将其应用于两个不同的子类型A.你必须为超类型实现自己的转换.

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

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

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 ()
           }
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

我们可以使用更高级的技巧来避免为每个伪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
Run Code Online (Sandbox Code Playgroud)

要构造新对象,我们将使用初始化内部数据的函数 - 相当于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
                           }
Run Code Online (Sandbox Code Playgroud)

请注意,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
Run Code Online (Sandbox Code Playgroud)


Rot*_*sor 8

我认为你的问题是你没有一个很好的方法来指定你正在操作的对象.为了解决这个问题,我建议使用一个单独的程序状态,包含两个对象:

data MainState = MainState { objA :: FieldsA, objB :: FieldsB }
Run Code Online (Sandbox Code Playgroud)

现在您的主要功能monad可能如下所示:

type Main t = StateT MainState IO t
Run Code Online (Sandbox Code Playgroud)

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

withObjA :: StateT FieldsA IO t -> Main t
withObjB :: StateT FieldsB IO t -> Main t
Run Code Online (Sandbox Code Playgroud)

用法如下:

test :: Main ()
test = do
    withObjA $ do
        setX_A 2
        printX_A
    withObjB $ do
        printX_A
        setX_B 5
        printX_B
Run Code Online (Sandbox Code Playgroud)

更新:

以下是如何实现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 })
Run Code Online (Sandbox Code Playgroud)

这里,函数withPart促进action对a part上的操作进行操作whole,使用getPart从整体中提取部分并setPart更新整体的一部分.如果有人告诉我库函数做类似的事情,我将不胜感激.withObjAwithObjB通过将各自的访问者函数传递给它们来实现withPart.