c++ - 프로그래밍 - 하스켈 장점




C++에서 하스켈 클래스 및 상태에 이르기까지 (2)

귀하의 문제는 당신이 어떤 개체를 조작하고 있는지를 지정하는 좋은 방법이 없다는 것입니다. 이 문제를 해결하기 위해 두 객체를 묶는 별도의 프로그램 상태를 사용하도록 제안합니다.

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

이제 주요 함수 모나드는 다음과 같이 보일 수 있습니다 :

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 구현하는 방법은 다음과 withObjB .

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 함수는 whole 에서 작동하는 액션에 대해 part 에서 작동하는 액션을 getPart 합니다. getPart 를 사용하여 whole 파트를 추출하고 setPart 를 사용하여 전체 파트를 업데이트합니다. 만약 누군가 내가 비슷한 것을하고있는 라이브러리 함수를 말해 주면 고맙겠습니다. withObjAwithObjB 는 각각의 접근 자 함수를 withPart 에 전달하여 구현됩니다.

이 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();
}

하스켈 코드로 변환하고 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() 테스트 중입니다.

이제 내가 가진 문제에 대해 : 리프트를 사용하면 함수가 StateT FieldsB 유형이되기 때문에 괜찮 FieldsB . 그러나 리프트없이 setX_A 를 사용하려고하면 문제가 발생합니다.

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

setX_A 유형을 두 번째 유형으로 변경하면 리프트와 함께 사용하면 클래스 B가 A에서 파생되기 때문에 작동하지 않습니다.


우선, 주어진 세부 사항에 대해 감사 드리며, 문제를 훨씬 쉽게 이해할 수 있습니다!

자, 당신이 여기에있는 접근법은 아마 이상적이지 않을 것입니다. 그것은 당신이 겪고있는 많은 어려움을 일으키는 원인이되는 각 객체를위한 새로운 StateT 를 소개하고, 더 많은 객체를 추가하면 점차적으로 상황이 악화 될 것입니다. 또한 문제를 복잡하게하는 것은 Haskell이 subtyping 에 대한 기본 개념을 가지고 있지 않고, 타입 클래스 컨텍스트를 모방하는 것이고, 일종의, 일종의, 서투른 것이고 최선이 아니라는 것이다.

이것이 매우 긴급한 스타일의 코드임을 깨닫고 그것을 Haskell로 직접 번역하는 것은 다소 어리석은 일입니다. 그래서 과제입니다. 표준 Haskell에 좀 더 가깝게하는 방법에 대해 이야기합시다.

명령형 코드

상태 모나드 스타일 :

이제 IO 제쳐두고 순수한 코드에서 이와 같은 작업을 수행하는 일반적인 방법은 다음과 같습니다.

  • 모든 상태를 포함하는 데이터 유형 만들기
  • getput 상태를 "수정"합니다.

출력을 위해, IO 주위의 StateT 를 사용하거나, 출력을 나타내는 상태 데이터에 필드를 추가하고, String 목록을 보유하고, 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

이것은 개념적으로 원본에 훨씬 더 가깝고, 질문에 대한 의견에서 @augustss가 제안한 행을 따릅니다.

약간의 IORef 은 객체를 간단한 값으로 유지하는 IORef 를 사용하여 현재 버전을 유지하는 것입니다. 두 접근법의 차이점은 OOP 언어에서 내부 상태와 변경할 수없는 객체를 변경할 수있는 setter 메소드가있는 변경 가능한 객체와 대략적으로 동일합니다.

사물

어려움의 나머지 절반은 하스켈에서 상속을 모델링하는 것입니다. 사용하는 접근법은 많은 사람들이 뛰어 넘는 가장 명백한 방법이지만 다소 제한적입니다. 예를 들어, 슈퍼 유형이 예상되는 모든 컨텍스트에서 오브젝트를 실제로 사용할 수 없습니다. 예를 들어 함수에 (A a) => a -> a -> Bool 과 같은 유형이있는 경우 (A a) => a -> a -> Bool 두 가지 하위 유형에이를 적용하는 간단한 방법이 없습니다. 당신은 슈퍼 타입으로 자신 만의 캐스팅을 구현해야 할 것입니다.

다음은 하스켈에서 사용하는 것이 더 자연스럽고 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 ()
           }

메소드를 제공하기 위해 유형 클래스를 사용하는 대신,이를 사용하여 수퍼 유형 에 대한 형변환을 제공합니다.

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 언어의 private 멤버와 같은 내부 데이터를 초기화하는 함수를 사용하여 객체를 나타내는 유형을 만듭니다.

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
                           }

newBnewB 호출하고 멤버 함수를 보유하는 데이터 유형을 가져옵니다. 그것은 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




monads