Незаконное объявление экземпляра / перекрывающиеся экземпляры

С учетом классов X и Y, какой самый идиоматический подход к созданию экземпляров классов друг друга? например -

instance (X a) => Y a where ...
instance (Y a) => X a where ...

Я хотел бы избежать расширений. Кроме того, я знаю, что это может вызвать некоторую неприятную бесконечную рекурсию, поэтому я открыт для совершенно другого подхода, чтобы выполнить то же самое и остаться относительно СУХИМ. Ниже дан некоторый контекст относительно точной проблемы, с которой я столкнулся -

data Dealer = Dealer Hand
data Player = Player Hand Cash

class HasPoints a where
    getPoints :: a -> Int

class (HasPoints a) => CardPlayer a where
    getHand :: a -> Hand

    viewHand :: a -> TurnIsComplete -> Hand

    hasBlackjack :: a -> Bool
    hasBlackjack player = getPoints player == 21 &&
                          (length . getCards . getHand) player == 2

    busts :: a -> Bool
    busts player = getPoints player > 21

Я хотел бы сделать это -

instance (CardPlayer a) => HasPoints a where
    getPoints = getPoints . getHand

Но, похоже, я должен сделать это -

instance HasPoints Dealer where
    getPoints = getPoints . getHand

instance HasPoints Player where
    getPoints = getPoints . getHand

РЕДАКТИРОВАТЬ

Кажется, мой любимый подход состоит в том, чтобы сохранить HasPoints Тип класса и реализовать CardPlayer как data вместо.

data CardPlayer = Dealer Hand | Player Hand Cash

instance HasPoints CardPlayer where
    getPoints = getPoints . getHand

getCash :: CardPlayer -> Maybe Cash
getHand :: CardPlayer -> Hand
viewHand :: CardPlayer -> TurnIsComplete -> Hand
hasBlackjack :: CardPlayer -> Bool
busts :: CardPlayer -> Bool

-- I wanted HasPoints to be polymorphic
-- so it could handle Card, Hand, and CardPlayer

instance HasPoints Hand where
    getPoints Hand { getCards = [] } = 0

    getPoints hand = if base > 21 && numAces > 0
                     then maximum $ filter (<=21) possibleScores
                     else base
      where base = sum $ map getPoints $ getCards hand
            numAces = length $ filter ((Ace==) . rank) $ getCards hand
            possibleScores = map ((base-) . (*10)) [1..numAces]

instance HasPoints Card where
    -- You get the point

2 ответа

Решение

С учетом классов X и Y, какой самый идиоматический подход к созданию экземпляров классов друг друга?

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

class HasPoints a where
    getPoints :: a -> Int

class (HasPoints a) => CardPlayer a where
    getHand :: a -> Hand
    viewHand :: a -> TurnIsComplete -> Hand
    hasBlackjack :: a -> Bool
    busts :: a -> Bool

Что у них общего? Все они принимают ровно одно значение типа параметра класса в качестве первого аргумента, поэтому при наличии такого значения мы можем применить к нему каждую функцию и получить всю ту же информацию, не беспокоясь об ограничении класса.

Так что если вы хотите хороший идиоматичный СУХОЙ подход, подумайте об этом:

data CardPlayer a = CardPlayer
    { playerPoints :: Int 
    , hand :: Hand
    , viewHand :: TurnIsComplete -> Hand
    , hasBlackjack :: Bool
    , busts :: Bool
    , player :: a
    }

data Dealer = Dealer
data Player = Player Cash

В этой версии типы CardPlayer Player а также CardPlayer Dealer эквивалентны Player а также Dealer типы у вас были. player Поле записи здесь используется, чтобы получить данные, специализированные для типа проигрывателя, а функции, которые были бы полиморфными с вашим классовым ограничением, могут просто работать со значениями типа CardPlayer a,

Хотя, возможно, это будет иметь больше смысла для hasBlackjack а также busts быть обычными функциями (например, ваши реализации по умолчанию), если только вам не нужно моделировать игроков, неуязвимых к стандартным правилам блэкджека.

С этой версии вы можете теперь определить HasPoints один класс, если у вас есть очень разные типы, которые должны быть его экземплярами, хотя я скептически отношусь к полезности этого, или вы можете применить то же преобразование, чтобы получить другой слой:

data HasPoints a = HasPoints
    { points :: Int
    , pointOwner :: a
    }

Тем не менее, этот подход быстро становится громоздким по мере того, как вы вкладываете подобные специализации.

Я бы предложил сбросить HasPoints полностью. Он имеет только одну функцию, которая просто извлекает Intтак что любой код, который обрабатывает HasPoints экземпляры, как правило, могут просто использовать Intи покончим с этим.

В общем случае невозможно объявить все экземпляры класса также экземплярами другого класса, не сделав проверку типов неразрешимой. Таким образом, предложенное вами определение будет работать только с UndecidableInstances включено:

{-# LANGUAGE FlexibleInstances, UndecidableInstances #-}

instance (CardPlayer a) => HasPoints a where
    getPoints = getPoints . getHand

Хотя можно пойти по этому пути, я бы предложил вместо этого рефакторинг кода следующим образом:

data Hand = ...

handPoints :: Hand -> Int
handPoints = ...

data Dealer = Dealer Hand
data Player = Player Hand Cash

class CardPlayer a where
  getHand :: a -> Hand
  ...

instance CardPlayer Dealer where ...
instance CardPlayer Player where ...

playerPoints :: (CardPlayer a) => a -> Int
playerPoints = handPoints . getHand
Другие вопросы по тегам