Незаконное объявление экземпляра / перекрывающиеся экземпляры
С учетом классов 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