我正在尝试通过实施简单的纸牌游戏来编写更多静态类型安全的代码。在该游戏中,存在若干唯一卡并且每张卡具有卡特定效果,其可能需要附加参数(例如,效果的目标)。一名玩家持有两张牌,并轮流选择其中一张牌,从而导致该牌的效果发生。
注意:这篇文章中的大部分细节都是在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)
}
然后我可以从中做到以下几点?
{{1}}
但我觉得我在兔子洞里走得太远了。我想要实现的目标是什么?有必要吗?我是否需要深入打字以确保静态类型安全,或者我错过了一些非常基本的东西?
答案 0 :(得分:0)
对于第一个问题,我会用代表关系的类型替换你的元组
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中很难 - 语言并不是真正为它设计的。如果你想尽可能地推动它,你最好使用像伊德里斯这样的东西。