您可能已经注意到,用类型表示多米诺骨牌很容易:
sealed trait One
sealed trait Two
sealed trait Three
sealed trait Four
sealed trait Five
sealed trait Six
sealed trait Domino[A, B] extends Product with Serializable
object Domino {
case object OneOne[One, One]
case object OneTwo[One, Two]
... // the other types of dominoes
}
如果你想有一个线性链也很容易:
sealed trait Chain[A, B] extends Product with Serializable
object Chain {
case class One[A, B](domino: Domino[A, B]) extends Chain[A, B]
case class Prepend[A, B, C](head: Domino[A, B], tail: Chain[B, C]) extends Chain[A, C]
}
如果这不是线性的,事情就会变得棘手。你可能想转弯。这样做的方法不止一种:
xxyy
yy
xx
xx
yy
xx
y
y
y
y
xx
并且它们中的每一个都必须表示为一个单独的案例。如果你想避免这样的事情:
f <- f tile would have to be over or under bb tile
aabbc f
e c
edd
您必须以某种方式检测到这种情况并阻止它。你有两个选择:
- 不要以类型表示,将其表示为值并使用一些智能构造函数来计算您的移动是否有效,并返回带有添加图块或错误的链
- 将每个回合表示为不同的类型,以类型级别表示,并需要一些证据才能创建图块。这应该是可能的,但要困难得多,并且需要您在编译时知道确切的类型(因此按需动态添加图块可能更难,因为您必须为每次移动预先准备好证据)
但是在多米诺骨牌中,除了转弯之外,我们还可以有分支:
aab
bdd
cc
如果你想用类型来表达它,现在你有一个可以附加到的两个头部(和一个可以附加到的尾部)。在游戏过程中,你可以拥有更多它们,所以你必须以某种方式表达两者:你有多少分支,以及你想向哪个分支添加新瓷砖。仍然可能,但会使您的代码更加复杂。
你可以例如用某种 HList 表达头(如果您使用的是无形的)并使用该表示来隐式告诉您要修改 HList 的哪个元素。
然而,在这一点上,类型级编程几乎没有什么好处:你必须提前知道你的类型,你很难动态添加新的瓦片,你必须以这样的方式保持状态,你会能够检索确切的类型,以便类型级别的证据可以工作......
因此,我建议使用一种仍然是类型安全但更容易接近的方法:只需使用smart constructors:
type Position = UUID
sealed trait Chain extends Product with Serializable
object Chain {
// prevent user from accessing constructors and copy directly
sealed abstract case class One private (
domino: Domino,
position: Position
) extends Chain
sealed abstract case class PrependLine private (
domino: Domino,
position: Position,
chain: Chain
)
sealed abstract case class Branch private (
chain1: Chain,
chain2: Chain
)
def start(domino: Domino): Chain
// check if you can add domino at this position, recursively rewrite tree
// if needed to add it at the right branch or maybe even create a new branch
def prepend(domino: Domino, to: Chain, at: Position): Either[Error, Chain]
}
这仍然无法创建“无效”的多米诺骨牌链。同时,添加新规则、扩展功能和在请求之间保持状态会更容易(您提到要构建服务器)。