我仍然试图围绕agda,所以我写了一个小小的tic-tac-toe游戏类型
data Game : Player -> Vec Square 9 -> Set where
start : Game x ( - ∷ - ∷ - ∷
- ∷ - ∷ - ∷
- ∷ - ∷ - ∷ [] )
xturn : {gs : Vec Square 9} -> (n : ℕ) -> Game x gs -> n < (#ofMoves gs) -> Game o (makeMove gs x n )
oturn : {gs : Vec Square 9} -> (n : ℕ) -> Game o gs -> n < (#ofMoves gs) -> Game x (makeMove gs o n )
这将保持有效的游戏路径。
此处#ofMoves gs
会返回空Square
的数量,
n < (#ofMoves gs)
将证明n
移动有效,
并且makeMove gs x n
替换了游戏状态向量中的n
空方格。
在对自己进行了一些激动人心的比赛之后,我决定拍摄一些更棒的东西。我们的目标是创造一个能够吸引x玩家和o玩家的功能,并在与死亡的史诗般的战斗中相互对抗。
--two programs enter, one program leaves
gameMaster : {p : Player } -> {gs : Vec Square 9} --FOR ALL
-> ({gs : Vec Square 9} -> Game x gs -> (0 < (#ofMoves gs)) -> Game o (makeMove gs x _ )) --take an x player
-> ({gs : Vec Square 9} -> Game o gs -> (0 < (#ofMoves gs)) -> Game x (makeMove gs o _ )) --take an o player
-> ( Game p gs) --Take an initial configuration
-> GameCondition --return a winner
gameMaster {_} {gs} _ _ game with (gameCondition gs)
... | xWin = xWin
... | oWin = oWin
... | draw = draw
... | ongoing with #ofMoves gs
... | 0 = draw --TODO: really just prove this state doesn't exist, it will always be covered by gameCondition gs = draw
gameMaster {x} {gs} fx fo game | ongoing | suc nn = gameMaster (fx) (fo) (fx game (s≤s z≤n)) -- x move
gameMaster {o} {gs} fx fo game | ongoing | suc nn = gameMaster (fx) (fo) (fo game (s≤s z≤n)) -- o move
此处(0 < (#ofMoves gs))
是“短手”,用于证明游戏正在进行,
gameCondition gs
将按照您的预期返回游戏状态(xWin
,oWin
,draw
或ongoing
之一
我想证明有有效的动作(s≤s z≤n
部分)。这应该是可能的,因为suc nn
&lt; = #ofMoves gs
。我不知道如何在agda中完成这项工作。
答案 0 :(得分:9)
我会尝试回答您的一些问题,但我不认为您从正确的角度来回答这个问题。虽然您当然可以使用显式证明使用有界数字,但您很可能会更成功地使用数据类型。
对于makeMove
(我已在答案的其余部分将其重命名为move
),您需要一个以可用空闲广场为界的数字。也就是说,当你有4个自由方格时,你希望能够只用0,1,2和3来调用move
。这是实现这一目标的一种非常好的方法。
查看Data.Fin
,我们发现了这种有趣的数据类型:
data Fin : ℕ → Set where
zero : {n : ℕ} → Fin (suc n)
suc : {n : ℕ} (i : Fin n) → Fin (suc n)
Fin 0
为空(zero
和suc
Fin n
n
Fin 1
大于或等于1)。 zero
只有Fin 2
,zero
有suc zero
和n
,依此类推。这恰好代表了我们所需要的:lookup : ∀ {a n} {A : Set a} → Fin n → Vec A n → A
lookup zero (x ∷ xs) = x
lookup (suc i) (x ∷ xs) = lookup i xs
所限定的数字。您可能已经看到这用于安全向量查找的实现:
lookup _ []
Fin 0
案例是不可能的,因为gameMaster
没有元素!
如何很好地应用这个问题?首先,我们必须跟踪我们有多少空方格。这允许我们证明Vec
确实是终止函数(空方块的数量总是在减少)。让我们写一个data Player : Set where
x o : Player
data SquareVec : (len : ℕ) (empty : ℕ) → Set where
[] : SquareVec 0 0
-∷_ : ∀ {l e} → SquareVec l e → SquareVec (suc l) (suc e)
_∷_ : ∀ {l e} (p : Player) → SquareVec l e → SquareVec (suc l) e
的变体,它不仅跟踪长度,还跟踪空方块:
Square
请注意,我删除了-∷_
数据类型;相反,空方块直接烘焙到- ∷ rest
构造函数中。而不是-∷ rest
我们有move
。
我们现在可以编写SquareVec
函数了。它的类型应该是什么?好吧,它会带Fin e
至少一个空方格,e
(其中Player
是空方格的数量)和Fin e
。 move : ∀ {l e} → Player → SquareVec l (suc e) → Fin (suc e) → SquareVec l e
move p ( -∷ sqs) zero = p ∷ sqs
move {e = zero} p ( -∷ sqs) (suc ())
move {e = suc e} p ( -∷ sqs) (suc fe) = -∷ move p sqs fe
move p (p′ ∷ sqs) fe = p′ ∷ move p sqs fe
保证我们这个函数总能找到合适的空方:
SquareVec
请注意,此函数为我们提供了一个Fin
,其中只填充了一个空方块。这个功能根本不能填充多个空格或没有空格!
我们走向向量寻找一个空方格;一旦我们找到它,zero
参数告诉我们它是否是我们要填写的方格。如果它是Game
,我们填写播放器;如果它不是,我们继续搜索向量的其余部分,但数字较小。
现在,游戏代表。感谢我们之前做的额外工作,我们可以简化move-p
数据类型。 Player
构造函数只是告诉我们移动发生在哪里以及它是什么!为简单起见,我删除了data Game : ∀ {e} → SquareVec 9 e → Set where
start : Game empty
move-p : ∀ {e} {gs} p (fe : Fin (suc e)) → Game gs → Game (move p gs fe)
索引;但它可以正常使用它。
empty
哦,- ∷ - ∷ ...
是什么?这是empty : ∀ {n} → SquareVec n n
empty {zero} = []
empty {suc _} = -∷ empty
的快捷方式:
GameCondition
现在,各州。我将状态分为可能正在运行的游戏状态和结束游戏的状态。同样,您可以使用原始data State : Set where
win : Player → State
draw : State
going : State
data FinalState : Set where
win : Player → FinalState
draw : FinalState
:
open import Data.Empty
open import Data.Product
open import Relation.Binary.PropositionalEquality
对于以下代码,我们需要这些导入:
-- Dummy implementation.
state : ∀ {e} {gs : SquareVec 9 e} → Game gs → State
state {zero} gs = draw
state {suc _} gs = going
确定游戏状态的功能。填写您的实际实施;这个让玩家玩,直到董事会完全充满。
State
接下来,我们需要证明当没有空方格时going
不能是zero-no-going : ∀ {gs : SquareVec 9 0} (g : Game gs) → state g ≢ going
zero-no-going g ()
:
state
同样,这是假人gameMaster
的证据,你实际实施的证据将会非常不同。
现在,我们拥有了实施o
所需的所有工具。首先,我们必须决定它的类型。与您的版本非常相似,我们将使用两个代表 AI 的函数,一个用于x
,另一个用于FinalState
。然后我们将采取游戏状态并生成AI : Set
AI = ∀ {e} {sqv : SquareVec 9 (suc e)} → Game sqv → Fin (suc e)
gameMaster : ∀ {e} {sqv : SquareVec 9 e} (sp : Player)
(x-move o-move : AI) → Game sqv →
FinalState × (Σ[ e′ ∈ ℕ ] Σ[ sqv′ ∈ SquareVec 9 e′ ] Game sqv′)
。在这个版本中,我实际上返回了最终的棋盘,以便我们可以看到游戏的进展情况。
现在,AI功能将返回他们想要的转弯,而不是返回整个新的游戏状态。这更容易使用。
支持自己,这是我提到的类型签名:
gameMaster sp xm om g with state g
... | win p = win p , _ , _ , g
... | draw = draw , _ , _ , g
... | going = ?
请注意,AI函数采用至少有一个空方格的游戏状态并返回一个移动。现在进行实施。
win
因此,如果当前状态为draw
或FinalState
,我们将返回相应的going
和当前的电路板。现在,我们必须处理e
案件。我们会在gameMaster {zero} sp xm om g | going = ?
gameMaster {suc e} x xm om g | going = ?
gameMaster {suc e} o xm om g | going = ?
(空方格的数量)上进行模式匹配,以确定游戏是否在最后:
zero
state
情况不可能发生,我们之前证明,当空方格的数量为零时,going
无法返回state g
。如何在这里申请证明?
我们在state g ≡ going
上匹配了模式,现在我们知道inspect
;但遗憾的是阿格达已经忘记了这些信息。这就是Dominique Devriese所暗示的:state g
机器允许我们保留证据!
而不仅仅是inspect state g
上的模式匹配,我们还会在gameMaster sp xm om g with state g | inspect state g
... | win p | _ = win p , _ , _ , g
... | draw | _ = draw , _ , _ , g
gameMaster {zero} sp xm om g | going | [ pf ] = ?
gameMaster {suc e} x xm om g | going | _ = ?
gameMaster {suc e} o xm om g | going | _ = ?
上模式匹配:
pf
state g ≡ going
现在证明了zero-no-going
,我们可以将其提供给gameMaster {zero} sp xm om g | going | [ pf ]
= ⊥-elim (zero-no-going g pf)
:
gameMaster
另外两个案例很简单:我们只应用AI函数并递归地将gameMaster {suc e} x xm om g | going | _
= gameMaster o xm om (move-p x (xm g) g)
gameMaster {suc e} o xm om g | going | _
= gameMaster x xm om (move-p o (om g) g)
应用于结果:
player-lowest : AI
player-lowest _ = zero
max : ∀ {e} → Fin (suc e)
max {zero} = zero
max {suc e} = suc max
player-highest : AI
player-highest _ = max
我写了一些愚蠢的AI,第一个填充了第一个可用的空方块;第二个填充了最后一个。
player-lowest
现在,让player-lowest
与C-c C-n gameMaster x player-lowest player-lowest start <RET>
匹配!在Emacs中,键入draw ,
0 ,
x ∷ (o ∷ (x ∷ (o ∷ (x ∷ (o ∷ (x ∷ (o ∷ (x ∷ [])))))))) ,
move-p x zero
(move-p o zero
(move-p x zero
(move-p o zero
(move-p x zero
(move-p o zero
(move-p x zero
(move-p o zero
(move-p x zero start))))))))
:
x
我们可以看到所有方块都已填充,并且它们在o
和player-lowest
之间交替显示。将player-highest
与draw ,
0 ,
x ∷ (x ∷ (x ∷ (x ∷ (x ∷ (o ∷ (o ∷ (o ∷ (o ∷ [])))))))) ,
move-p x zero
(move-p o (suc zero)
(move-p x zero
(move-p o (suc (suc (suc zero)))
(move-p x zero
(move-p o (suc (suc (suc (suc (suc zero)))))
(move-p x zero
(move-p o (suc (suc (suc (suc (suc (suc (suc zero)))))))
(move-p x zero start))))))))
匹配可为我们提供:
Fin
如果你真的想使用证明,那么我建议Fin₂ : ℕ → Set
Fin₂ n = ∃ λ m → m < n
fin⟶fin₂ : ∀ {n} → Fin n → Fin₂ n
fin⟶fin₂ zero = zero , s≤s z≤n
fin⟶fin₂ (suc n) = map suc s≤s (fin⟶fin₂ n)
fin₂⟶fin : ∀ {n} → Fin₂ n → Fin n
fin₂⟶fin {zero} (_ , ())
fin₂⟶fin {suc _} (zero , _) = zero
fin₂⟶fin {suc _} (suc _ , s≤s p) = suc (fin₂⟶fin (_ , p))
的以下表示:
inspect
与问题没有严格关联,但是with
使用了相当有趣的技巧,值得一提。要理解这个技巧,我们必须先了解with
的工作原理。
当您在表达式expr
上使用expr
时,Agda会查看所有参数的类型,并使用新变量替换w
的任何出现,让我们称之为test : (n : ℕ) → Vec ℕ (n + 0) → ℕ
test n v = ?
。例如:
v
此处,Vec ℕ (n + 0)
的类型为test : (n : ℕ) → Vec ℕ (n + 0) → ℕ
test n v with n + 0
... | w = ?
,正如您所料。
n + 0
但是,一旦我们对v
进行抽象,Vec ℕ w
的类型会突然变为n + 0
。如果您以后想要在其类型中使用包含gameMaster
的内容,则替换不会再次发生 - 这是一次性交易。
在with
函数中,我们将state g
应用于going
并匹配模式以找出它zero-no-going
。当我们使用state g
时,going
和state g ≡ state g
是两个与Agda无关的独立事物。
我们如何保存这些信息?我们需要获得with
并且state g
仅替换state g ≡ going
- 这将为我们提供所需的inspect
。
state g
所做的是它隐藏了函数应用程序hide
。我们必须以一种Agda看不到hide state g
和state g
实际上是相同的方式编写函数A
。
隐藏某些内容的一种可能方法是使用以下事实:对于任何类型A
,⊤ → A
和⊤
都是同构的 - 也就是说,我们可以自由地从一个表示转到其他而不会丢失任何信息。
但是,我们无法使用标准库中定义的data Unit : Set where
unit : Unit
。我会在一瞬间说明原因,但就目前而言,我们将定义一种新类型:
Hidden : Set → Set
Hidden A = Unit → A
隐藏价值意味着什么:
reveal
我们可以通过将unit
应用于其中来轻松reveal : {A : Set} → Hidden A → A
reveal f = f unit
隐藏价值:
hide
我们需要采取的最后一步是hide : {A : Set} {B : A → Set} →
((x : A) → B x) → ((x : A) → Hidden (B x))
hide f x unit = f x
功能:
⊤
为什么这不适用⊤
?如果您将tt
声明为记录,Agda可以自行确定hide f x
是唯一的值。因此,当面对λ _ → f x
时,Agda不会停止在第三个参数(因为它已经知道它必须是什么样子)并自动将其减少到data
。使用hide f x
关键字定义的数据类型不具备这些特殊规则,因此reveal
仍然保持这种状态,直到有人f x
它并且类型检查程序无法看到那些&#39}。 hide f x
内的sa data Reveal_is_ {A : Set} (x : Hidden A) (y : A) : Set where
[_] : (eq : reveal x ≡ y) → Reveal x is y
inspect : {A : Set} {B : A → Set}
(f : (x : A) → B x) (x : A) → Reveal (hide f x) is (f x)
inspect f x = [ refl ]
子表达式。
其余的只是安排东西,所以我们可以在以后得到证据:
inspect state g : Reveal (hide state g) is (state g)
-- pattern match on (state g)
inspect state g : Reveal (hide state g) is going
所以,你有它:
reveal
当您hide state g
state g
后,您将获得state g ≡ going
,最后获得{{1}}的证据。
答案 1 :(得分:3)
我认为您正在寻找一种名为“inspect”或“类固醇检查”的Agda技术。它允许您获取从模式匹配中学到的知识的相等证明。我建议您阅读以下邮件中的代码,并尝试了解它是如何工作的。关注底部的函数foo如何记住“f x = z”并通过匹配“inspect(hide f x)”与“f x”匹配来实现:
https://lists.chalmers.se/pipermail/agda/2011/003286.html
要在实际代码中使用它,我建议您从Agda标准库导入Relation.Binary.PropositionalEquality并使用其版本的inspect(表面上与上面的代码不同)。它有以下示例代码:
f x y with g x | inspect g x
f x y | c z | [ eq ] = ...
注意:“检查类固醇”是检查习语中旧方法的更新版本。
我希望这会有所帮助......