OO接口转换为Haskell

Eug*_*gen 7 modeling haskell abstract-data-type typeclass

我的具体问题实际上并不是关于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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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


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

scl*_*clv 8

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

data Strategy state = Strategy {
                 initialState :: state,
                 extractGuess :: state -> Guess,
                 updateState  :: Answer -> state -> state
         }
Run Code Online (Sandbox Code Playgroud)

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

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

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

type packedStrategy = Oracle -> Int

packStrategy :: Strategy state -> PackedStrategy
packStrategy str oracle = length $ stepMany str oracle
Run Code Online (Sandbox Code Playgroud)

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

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