在 Scala 中键入安全的多米诺骨牌
Type safe dominoes in scala
所以我正在尝试编写一个多米诺骨牌游戏服务器,我正在编写我的核心类型、瓷砖和多米诺骨牌集,我突然想到,包括瓷砖点的类型信息可以让我写一个创建多米诺骨牌链的函数要简单得多,由于我无法弄清楚这一点,我已经开始并多次离开这个项目未完成,希望有人有一个简单的类型安全的瓷砖表示,从而导致一个简单的多米诺骨牌链功能。原因是我目前的心理模型是多米诺骨牌游戏中的棋盘只是一个初始瓷砖和 1-3 条多米诺骨牌链,每个链开始并匹配初始瓷砖上的点数。
非常感谢,对于我的问题中的任何不完善之处,我们深表歉意。
sealed case class DoubleSix[L >: Nat, R <: Nat](lPips: Int, rPips: Int) extends Tile[L, R]
object DoubleSixSet {
val zeroZero: DoubleSix[_0, _0] = DoubleSix(0, 0)
}
对类型安全链接函数的较早尝试。
trait DominoChain[End] {
// val hasSpinner: Boolean
}
case object End extends DominoChain[Nothing] {
// val hasSpinner = false
}
case class Chain[D0, D1, X](head: Domino[D0, D1], tail: DominoChain[Domino[D1, X]]) extends DominoChain[Domino[D0, D1]] {
def hasSpinner =
if (head.isDouble || rest.hasSpinner) true
else false
}
正如您可能注意到的那样,用类型表示多米诺骨牌很容易:
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
您将不得不以某种方式检测并防止这种情况。您在这里有 2 个选项:
- 不要用类型来表达它,用值来表达它并使用一些智能构造函数来计算你的移动是否有效并且 return 带有添加的图块或错误的链
- 将每个回合表示为不同的类型,以类型级别表示并需要一些证据才能创建图块。这应该是可能的,但要困难得多,并且需要您在编译时知道确切的类型(因此按需动态添加 tile 可能会更难,因为您必须为每次移动预先准备好证据)
但是在多米诺骨牌中除了轮流我们还可以有分支:
aab
bdd
cc
如果你想用字体表达它,现在你有两个头可以放在前面(还有一个尾可以追加)。在游戏过程中,您可能会拥有更多分支,因此您必须以某种方式表达两者:您有多少个分支,以及您想向哪个分支添加新板块。仍然有可能,但会使您的代码更加复杂。
你可以,例如用某种 HList 表达头部(如果您使用无形)并使用该表示提供隐式告诉您要修改 HList 的哪个元素。
然而此时类型级编程的好处很少:你必须提前知道你的类型,你将难以动态添加新的图块,你将不得不以这种方式持久化状态将能够检索确切的类型,以便类型级别的证据起作用...
正因为如此,我建议一种仍然是类型安全的方法,同时更容易接近:只需使用 :
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]
}
这仍然无法创建“无效”的多米诺骨牌链。同时,添加新规则、扩展功能和在请求之间持久化状态将变得更加容易(您提到要构建服务器)。
所以我正在尝试编写一个多米诺骨牌游戏服务器,我正在编写我的核心类型、瓷砖和多米诺骨牌集,我突然想到,包括瓷砖点的类型信息可以让我写一个创建多米诺骨牌链的函数要简单得多,由于我无法弄清楚这一点,我已经开始并多次离开这个项目未完成,希望有人有一个简单的类型安全的瓷砖表示,从而导致一个简单的多米诺骨牌链功能。原因是我目前的心理模型是多米诺骨牌游戏中的棋盘只是一个初始瓷砖和 1-3 条多米诺骨牌链,每个链开始并匹配初始瓷砖上的点数。
非常感谢,对于我的问题中的任何不完善之处,我们深表歉意。
sealed case class DoubleSix[L >: Nat, R <: Nat](lPips: Int, rPips: Int) extends Tile[L, R]
object DoubleSixSet {
val zeroZero: DoubleSix[_0, _0] = DoubleSix(0, 0)
}
对类型安全链接函数的较早尝试。
trait DominoChain[End] {
// val hasSpinner: Boolean
}
case object End extends DominoChain[Nothing] {
// val hasSpinner = false
}
case class Chain[D0, D1, X](head: Domino[D0, D1], tail: DominoChain[Domino[D1, X]]) extends DominoChain[Domino[D0, D1]] {
def hasSpinner =
if (head.isDouble || rest.hasSpinner) true
else false
}
正如您可能注意到的那样,用类型表示多米诺骨牌很容易:
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
您将不得不以某种方式检测并防止这种情况。您在这里有 2 个选项:
- 不要用类型来表达它,用值来表达它并使用一些智能构造函数来计算你的移动是否有效并且 return 带有添加的图块或错误的链
- 将每个回合表示为不同的类型,以类型级别表示并需要一些证据才能创建图块。这应该是可能的,但要困难得多,并且需要您在编译时知道确切的类型(因此按需动态添加 tile 可能会更难,因为您必须为每次移动预先准备好证据)
但是在多米诺骨牌中除了轮流我们还可以有分支:
aab
bdd
cc
如果你想用字体表达它,现在你有两个头可以放在前面(还有一个尾可以追加)。在游戏过程中,您可能会拥有更多分支,因此您必须以某种方式表达两者:您有多少个分支,以及您想向哪个分支添加新板块。仍然有可能,但会使您的代码更加复杂。
你可以,例如用某种 HList 表达头部(如果您使用无形)并使用该表示提供隐式告诉您要修改 HList 的哪个元素。
然而此时类型级编程的好处很少:你必须提前知道你的类型,你将难以动态添加新的图块,你将不得不以这种方式持久化状态将能够检索确切的类型,以便类型级别的证据起作用...
正因为如此,我建议一种仍然是类型安全的方法,同时更容易接近:只需使用
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]
}
这仍然无法创建“无效”的多米诺骨牌链。同时,添加新规则、扩展功能和在请求之间持久化状态将变得更加容易(您提到要构建服务器)。