繁体   English   中英

OO接口转换为Haskell

[英]OO Interface translation to Haskell

我的具体问题实际上并不是关于Oos接口到Haskell的一般转换。 这是我能想到的最好的头衔。 然而,我确信我的问题源于对使用Haskell的建模代码仍然很差的理解,以及仍然位于OO范例之内的思维模式(仍然是一个haskell初学者,你看)。

我正在编写一个Mastermind(变异)模拟来测试几种Mastermind策略的适用性。 事实上,我已经在JavaLua中做到了这一点,因此这个Haskell版本只是我学习在Haskell中编程的练习。 如果您对我最终想要实现的目标感兴趣,可以查看Lua / Java版本的自述文件。

但现在我的具体问题(简而言之,以OO术语):我想提供一个策略接口,以便我可以互换地将一个遵循该接口的策略放入模拟递归(循环)中,并在完成后接收一些数据关于战略的表现。 另外,我想让策略保持任意状态,我不想关心每个策略保持什么样的状态。 但正是这个决定 - 实际上是必不可少的 - 使一切变得复杂。 另一个具体导致下面描述的问题的要求是,策略名称可以作为命令行参数提供,然后模拟以该特定策略运行。

起初我认为是适合这些要求的类型类,但在没有想出如何以这种方式对代码建模的真实想法之后我放弃了这个想法。 然后我决定使用ADT,从那时起就使用它并且代码相对较远 - 直到现在。


所以,表面上的问题是如何解决我在下面提供的问题。 更深层次的问题是如何在Haskell中更好地建模我对具有任意状态的接口的需求。

以下是我的代码中的简化和改编摘录:

-- reduced & simplified example
import Control.Monad.State

type Code = [Int]

data Answer = Answer { 
    blacks :: Int, 
    whites :: Int 
    } deriving (Eq, Show)

-- As you can see I decided for a type variable a that
-- represents the arbitrary state a strategy might carry
-- around. I wonder if this is the right way to do it.
-- | This is the interface for a strategy. A strategy must provide a method 
-- that, given a mastermind answer, returns the next guess, an initial state 
-- and the very first guess.
data Strategy a = Strategy {
    firstGuess :: Int -> Code,
    initialize :: Int -> a, -- a "constructor" in disguise
    guess      :: Answer -> State a Code
    }

dummy = Strategy {
    firstGuess   = firstGuess',
    initialize   = initialize', 
    guess        = guess'
    }

-- | The very first guess is always T0,T1,...,Tx, where x is the code length.
firstGuess' :: Int -> Code
firstGuess' length = [0..length-1]

-- | Memorize the code length
initialize' :: Int -> Int
initialize' = id

-- | Always return the same guess
guess' :: Answer -> State Int Code
guess' = const $ liftM firstGuess' get

-- HERE IS THE PROBLEM
-- I need this function since I'll get the name of a strategy
-- as a string from the command line and need to dispatch the
-- correct strategy to the simulation. Note, that there would
-- be of course more pattern matches for different strategies
-- with different accompanying states a.
nameToStrategy :: String -> Strategy a
nameToStrategy "dummy" = dummy

执行该文件会产生以下错误消息:

Prelude> :l Problem.hs
[1 of 1] Compiling Main             ( Problem.hs, interpreted )

Problem.hs:37:25:
    Couldn't match expected type `a' against inferred type `Int'
      `a' is a rigid type variable bound by
          the type signature for `nameToStrategy' at Problem.hs:36:37
      Expected type: Strategy a
      Inferred type: Strategy Int
    In the expression: dummy
    In the definition of `nameToStrategy':
        nameToStrategy "dummy" = dummy
Failed, modules loaded: none.

我可以直观地理解这个问题。 问题似乎是nameToStrategy不能只返回一个具有某种状态a策略a 类型变量必须具体,因为如果我将nameToStrategy的类型nameToStrategyString -> Strategy Int一切都很好。 但这不是解决我的问题的方法。

我想我需要放松一下这种类型。 但是,我真的不知道该怎么做。 我听说过Data.Dynamic和存在类型,这些可能对我有帮助。 不过,我觉得通过更好的代码建模,我不需要那些。


编辑 :我设法将sclv的建议结合到代码中,现在它好多了。 策略的代码更清晰,因为我不再需要第一次猜测的特殊情况,我可以使用警卫来更好地区分正确和不正确猜测的情况。 主要的模拟处理并不像sclv的版本那样优雅,因为我将stepState (以及使用stepState的函数)放入IO Monad来测量计算时间,从而产生一些“monadic语法噪声”。 能够轻松模拟几个模拟步骤(之前实际上不可能)帮助我找到了一个相互递归的无限循环( 这个bug很难理解 )。 总而言之,代码现在感觉更加离散。 毋庸置疑,我不再需要unsafeCoerce hack来将名称分配给策略(或更好的“打包策略”)。 我希望有朝一日思考的功能性思维方式对我来说也是很自然的。

好吧,让我们从头开始吧。 纯策略是一种在知识状态下产生猜测的函数。 state -> Guess 对于任何给定的状态,有一些方法可以为其添加新知识 - Answer -> state -> state 我们现在只需要一个初始状态,而不是初步猜测。

data Strategy state = Strategy {
                 initialState :: state,
                 extractGuess :: state -> Guess,
                 updateState  :: Answer -> state -> state
         }

所以现在让我们看看构成这些函数时会有什么好处。

type Oracle = Guess -> Maybe Answer -- we'll encode success as Nothing

stepState :: Oracle -> Strategy state -> state -> Maybe state
stepState oracle str state = fmap (\ans -> updateState str ans state) $ 
                                      oracle (extractGuess str state)

stepMany :: Strategy state -> Oracle -> [state]
stepMany str oracle = go (initialState str)
      where go state = case stepState oracle str state of
               Nothing -> []
               Just newState -> newState : go newState

因此, stepMany是我们想要的90%,但它在那个讨厌的状态参数中仍然是多态的。 这很容易解决 - 毕竟我们想要的步骤数,而不是步骤本身的中间状态。

type packedStrategy = Oracle -> Int

packStrategy :: Strategy state -> PackedStrategy
packStrategy str oracle = length $ stepMany str oracle

现在你可以写[packStrategy stratOne, packStrategy stratTwo]等。在此过程中,我们发现了一些重要的东西 - 你从策略中关心的只是它是一个来自某个问题的函数(由oracle代表)解决问题所需的步骤。 产生这种策略的一种方式(不是唯一的方法)是提供一种询问新知识(猜测)的方法和一种更新我们知识的方法(更新状态)。

这不是唯一的答案,也许并不适合您的目的,但它应该有助于您使用功能和类型而不是对象和功能进行思考。

您可以使用GADT(广义代数数据类型)和存在(下面的“forall a。”)完全按照您的要求进行操作。 “策略”类型隐藏内部类型“a”,这是一个实现细节。 调用“go”中的模式匹配将所有策略部分纳入范围。 请注意,我使用GHC的RecordWildCards“{..}”来保存我的手指。 这是因为“go”不会返回任何暴露内部类型“a”的内容。

GHC用户手册中有更多细节。

{-# LANGUAGE GADTs, RankNTypes, RecordWildCards #-}
import Control.Monad.State

type Code = [Int]

data Answer = Answer { blacks :: Int, whites :: Int } 
  deriving (Eq, Show)

data Strategy where
    Strategy :: forall a. { strategyName :: String
                          , firstGuess   :: Int -> Code
                          , initialize   :: Int -> a
                          , guess        :: Answer -> State a Code
                          } 
             -> Strategy

dummy = Strategy { strategyName = "dummy"
                 , firstGuess   = firstGuess'
                 , initialize   = initialize'
                 , guess        = guess'
                 }

-- | The very first guess is always T0,T1,...,Tx, where x is the code length.
firstGuess' :: Int -> Code
firstGuess' length = [0..length-1]

-- | Memorize the code length
initialize' :: Int -> Int
initialize' = id

-- | Always return the same guess
guess' :: Answer -> State Int Code
guess' = const $ liftM firstGuess' get

-- Take size and strategy and compute number of steps to win
-- modified to create local type variable 'a' to write type for 'step'
go :: Code -> Strategy -> (String,Int)
go secretCode (Strategy {initialize=initialize::Int->a,..}) =
  let size = length secretCode
      nextAnswer :: Code -> Maybe Answer
      nextAnswer _ = undefined {- compare with secretCode -}

      step :: Code -> Int -> State a (String,Int)
      step code n = case nextAnswer code of
                      Nothing -> return (strategyName,n)
                      Just answer -> do
                        code' <- guess answer
                        step code' $! (succ n)

  in evalState (step (firstGuess size) 0) (initialize size)

通过使用WriterT,我可以添加一个猜测日志:

-- Take size and strategy and compute number of steps to win
goW :: Code -> Strategy -> ((String,Int),[(Code,Answer)])
goW secretCode (Strategy {..}) =
  let size = length secretCode
      nextAnswer :: Code -> Maybe Answer
      nextAnswer _ = undefined {- compare with secretCode -}

      step code n = case nextAnswer code of
                      Nothing -> return (strategyName,n)
                      Just answer -> do
                        code' <- lift (guess answer)
                        tell [(code,answer)]
                        step code' $! (succ n)

  in evalState (runWriterT (step (firstGuess size) 0)) (initialize size)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM