我的具体问题实际上并不是关于Oos接口到Haskell的一般转换。这是我能想到的最好的头衔。然而,我确信我的问题源于对Haskell建模代码的理解仍然很差,并且仍然存在于OO范例中的思维模式(仍然是一个haskell初学者,你看)。
我正在编写一个Mastermind(变异)模拟来测试几种Mastermind策略的适用性。事实上,我已经在Java和Lua中做到了这一点,因此这个Haskell version只是让我学习在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
的策略。类型变量必须是
具体,因为如果我将nameToStrategy
的类型更改为String -> Strategy Int
一切都很好。但这不是我问题的解决方案。
我想我需要放松这种类型。但是,我真的不知道该怎么做。我听说过Data.Dynamic
和存在类型,这些可能对我有帮助。不过,我觉得通过更好的代码建模,我不需要那些。
编辑:我设法将sclv的建议结合到代码中,现在好多了。策略的代码更清晰,因为我不再需要第一次猜测的特殊情况,我可以使用警卫来更好地区分正确和不正确猜测的情况。主要的模拟处理并不像sclv的版本那样优雅,因为我将stepState
(以及使用stepState
的函数)放入IO Monad来测量计算时间,从而产生一些“monadic语法噪音”。能够轻松模拟几个模拟步骤(之前实际上不可能)帮助我找到一个相互递归的无限循环(这个bug很难理解)。总而言之,代码现在感觉更加离散。毋庸置疑,我不再需要unsafeCoerce
hack来将名称分配给策略(或更好的“打包策略”)。我希望有朝一日思考的功能性思维方式对我来说也是很自然的。
答案 0 :(得分:8)
好的,让我们从头开始。纯策略是一种在知识状态下产生猜测的函数。 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代表) )解决问题所需的步骤。产生这种策略的一种方式(不是唯一的方法)是提供一种询问新知识(猜测)的方法和一种更新我们知识的方法(更新状态)。
这不是唯一的答案,可能不适合您的目的,但它应该有助于您使用功能和类型而不是对象和功能进行思考。
答案 1 :(得分:2)
您可以使用GADT(广义代数数据类型)和存在(下面的“forall a。”)完全按照您的要求进行操作。 “策略”类型隐藏内部类型“a”,这是一个实现细节。调用“go”中的模式匹配将所有策略部分纳入范围。请注意,我使用GHC的RecordWildCards“{..}”来保存我的手指。这是因为“go”不会返回任何暴露内部类型“a”的内容。
GHC User Manual中存在更多细节。
{-# 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)