我正在尝试通过实现一个简单的纸牌游戏来编写更静态的类型安全代码。在这个游戏中,有几张独特的牌,每张牌都有一个特定于牌的效果,这可能需要额外的参数(例如,效果的目标)。一名玩家持有两张牌,然后选择玩其中一张,从而产生该牌的效果。
注意:这篇文章中的大多数细节都是在REPL中尝试的。我写了一个不太静态的类型安全的实现,但我想在完全投入之前确保我想要的是可行的。
以下是一些相关的定义:
trait CardEffectParams
case class OneTarget(player: Player) extends CardEffectParams
case class TwoTargets(player1: Player, player2: Player) extends CardEffectParams
// ...
trait Card {
// the parameters to use are specific to the card
type Params <: CardEffectParams
}
trait Hand {
case class CardInHand(card: Card) { /* with ctor not accessible from outside */ }
// a player can hold two cards
val card1: CardInHand
val card2: CardInHand
}
我想把打哪张牌的选择委托给一些策略,这样我就可以看到不同的策略是如何比较的。这就是我遇到的问题:我想把你可以返回的卡片限制在参数中传递的Hand
对象中的卡片上,我可以通过将其键入为hand.CardInHand
:来实现这一点
trait Strategy {
def choose(hand: Hand, gameState: GameState): hand.CardsInHand
}
但我也想传递额外的参数:例如,一张牌可能只允许我瞄准一名玩家(例如,跳过他们的回合),但另一张牌则可能允许我瞄准两名玩家(如,交换他们的牌)。这些由CardEffectParams
建模。所以我想返回hand.CardsInHand
和cardInHand.card.Params
,其中cardInHand
是我返回的实例,类似于这样:
/* NOT valid scala */
trait Strategy {
def choose(hand: Hand, gameState: GameState): (c: hand.CardsInHand, c.card.Params)
}
所以第一个问题是,这能做到吗?你将如何表达这种关系?
我还纠结于如何实例化CardEffectParams
子类,因为每个子类可能有不同的参数列表。我的第一个想法是进行模式匹配,但这失败了,因为匹配块的类型是所有可能结果的共同祖先:
case object CardA extends Card {
type Params = OneTarget
}
case object CardB extends Card {
type Params = TwoTargets
}
object RandomStrategy extends Strategy {
def choose(hand: Hand, gameState: GameState) = {
val card: Card = /* randomly pick card1 or card2 */
/* the type of the match block is CardEffectParams, not card.Params */
val param: card.Params = card match {
case CardA => OneTarget(...)
case CardB => TwoTargets(...)
}
}
}
我目前的想法是在每个卡对象中定义一个工厂方法,该方法接受参数列表,从中生成正确的类型:
trait Card {
type Params <: CardEffectParams
type HListTypeOfParams = /* insert shapeless magic */
def create[L <: HListTypeOfParams](l: L): Params
}
然后我可以从中执行以下操作?
// no idea if this works or not
val card: Card = ...
val params: card.Params = card match {
case c: CardA => c.create(1 :: HNil)
case c: CardB => c.create(1 :: 2 :: HNil)
}
但我觉得我在兔子洞里走得太远了。我想要实现的目标可能实现吗?有必要吗?我需要深入打字以确保静态打字安全吗?还是我错过了一些真正基本的东西?
对于第一个问题,我将用表示关系的类型替换您的元组
trait CardAndParams {
type C <: Card
val card: C
val params: C#Params
}
def choose[R <: CardAndParams](hand: Hand, gameState: GameState)(
implicit helper: Helper {type Out = R}): R
您将需要使用像我的Helper
示例这样的隐含词来驱动实际的策略实现,并确保推断出正确的R。这也是进行类型级计算的更常见的方法:
sealed trait RandomStrategyHelper[C <: Card] {
def params(): C#Params
}
object RandomStrategyHelper {
implicit def forCardA = new RandomStrategyHelper[CardA] {
def params() = 1 :: HNil
}
implicit def forCardB = new RandomStrategyHelper[CardB] {
def params() = 1 :: 2 :: HNil
}
}
def randomParams[C <: Card](card: C)(implicit rsh: RandomStrategyHelper[C]) =
rsh.params()
但我想你需要一种方法来从随机生成的卡片转移到强类型卡片,因此模式匹配似乎是合适的,因为很难在类型级别上表示随机卡片。
一般来说,这种类型级别的编程是可能的,但在Scala中很难——这种语言并不是专门为它设计的。如果你想把它推向极致,你最好使用像Idris这样的东西。