ОО Интерфейс перевод на Хаскелл - PullRequest
7 голосов
/ 29 марта 2011

Моя конкретная проблема на самом деле не в общем переводе интерфейса OO на Haskell. Это просто лучшее название, которое я мог придумать. Тем не менее, я уверен, что моя проблема возникла из-за все еще плохого понимания кода моделирования с помощью Haskell и мышления, все еще находящегося в стране парадигм OO (вы все еще начинающий в Haskell, видите ли).

Я пишу симулятор Mastermind (вариации), чтобы проверить работоспособность нескольких стратегий Mastermind. На самом деле, я уже делал это в Java и Lua , и, таким образом, эта версия Haskell - это всего лишь упражнение для меня, чтобы научиться программировать на Haskell. Вы можете проверить readme версии Lua / Java, если вас интересует то, чего я пытаюсь достичь в конце.

Но теперь для моей конкретной проблемы (вкратце и в терминах ОО): я хочу предоставить интерфейс для стратегий, чтобы я мог взаимозаменяемо поместить стратегию, которая придерживается этого интерфейса, в рекурсию моделирования (цикл) и после того, как это будет сделано получить некоторые данные об эффективности стратегии. Кроме того, я хочу, чтобы стратегия поддерживала произвольное состояние, и я не хочу заботиться о том, какое состояние поддерживает каждая стратегия. Но именно это решение, которое на самом деле необходимо, все усложнило. Другое требование, которое конкретно привело к описанной ниже проблеме, заключается в том, что имя стратегии может быть предоставлено в качестве аргумента командной строки, а затем выполняется симуляция с этой конкретной стратегией.

Сначала я счел класс типов подходящим для этих требований, но, не придумав реальной идеи, как смоделировать код таким образом, я отказался от этой идеи. Затем я выбрал 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) в монаду ввода-вывода, чтобы измерить время вычислений и, таким образом, иметь некоторый "монадический синтаксический шум". Возможность легко смоделировать пару шагов симуляции (что раньше было невозможно) помогла мне найти взаимно рекурсивный бесконечный цикл (, эту ошибку было странно понимать ). В целом, код теперь кажется более дискретным. Излишне говорить, что мне больше не нужен взлом unsafeCoerce, чтобы отправлять имена в стратегии (или лучше «упакованные стратегии»). Я надеюсь, что когда-нибудь функциональный образ мышления придет ко мне тоже.

Ответы [ 2 ]

8 голосов
/ 29 марта 2011

Хорошо, давайте начнем с нуля.Чистая стратегия - это функция, которая, учитывая состояние знаний, дает предположение.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] и т. Д. И по пути,мы обнаружили кое-что важное - в вашей стратегии вас волнует только то, что это функция от какой-то проблемы (представленной оракулом) до шагов, необходимых для ее решения.И один из способов (не единственный способ) создания таких стратегий - предоставить способ запрашивать новые знания (угадать) и способ обновить наши знания (состояние обновления).

Это не единственный ответ и, возможно, не идеальный для ваших целей, но он должен помочь вам перейти к мышлению с функциями и типами, а не объектами и возможностями.

2 голосов
/ 29 марта 2011

Вы можете делать в точности то, что вы хотите, используя GADT (Обобщенные алгебраические типы данных) и экзистенциалы («далее» ниже). Тип «Стратегия» скрывает внутренний тип «а», который является деталью реализации. Сопоставление с образцом в вызове «go» приводит все части стратегии в область видимости. Обратите внимание, что я использовал RecordWildCards GHC "{..}", чтобы сохранить свои пальцы. Это компилируется, потому что "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)
...