在agda中使用计算函数的值作为证明

时间:2013-08-28 02:01:02

标签: tic-tac-toe proof agda

我仍然试图围绕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将按照您的预期返回游戏状态(xWinoWindrawongoing之一

我想证明有有效的动作(s≤s z≤n部分)。这应该是可能的,因为suc nn&lt; = #ofMoves gs。我不知道如何在agda中完成这项工作。

2 个答案:

答案 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为空(zerosuc Fin n n Fin 1大于或等于1)。 zero只有Fin 2zerosuc zeron,依此类推。这恰好代表了我们所需要的: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 emove : ∀ {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

因此,如果当前状态为drawFinalState,我们将返回相应的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-lowestC-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

我们可以看到所有方块都已填充,并且它们在oplayer-lowest之间交替显示。将player-highestdraw , 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时,goingstate g ≡ state g是两个与Agda无关的独立事物。

我们如何保存这些信息?我们需要获得with并且state g仅替换state g ≡ going - 这将为我们提供所需的inspect

state g所做的是它隐藏了函数应用程序hide。我们必须以一种Agda看不到hide state gstate 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 ] = ...

注意:“检查类固醇”是检查习语中旧方法的更新版本。

我希望这会有所帮助......