如何在 Agda 中定义满足上下界约束的自然数类型?
可以用 record 保存自然数 x,以及 m ≤ x 和 x < n 的证明。数独中的数字 1 到 9 对应半开区间 [1, 10),因此可以定义为 Range 1 10。
原问题中的 InRange m n x 也能表达区间约束:它是给定的 x 满足区间条件的证明。若要让类型的值直接携带数字,需要把 x 和证明放在一起。
为什么原来的 Range 会接受无效区间?
问题出在 rbase 的类型:
rbase : {m n : ℕ} → Range m (suc n)
它允许任意的 m 和 n,既不要求下界小于上界,也不要求提供一个位于区间内的数字。取 m = 3、n = 1,得到的就是 Range 3 2,所以 test = rbase 可以成立。
后面的 rsuc 无法补上这个约束,因为无效的值已经可以通过 rbase 直接构造。
Range 3 2 作为类型表达式可以合法存在。合理的区间定义应让它没有可构造的值,而不是要求这个类型表达式本身非法。
用 record 保存数字及边界证明
下面的定义使用 Agda 标准库中的自然数和序关系。字段 lower 和 upper 的类型可以依赖前面定义的 value,这是依赖 record 的用法。Agda record 文档
module BoundedNumbers where
open import Data.Nat.Base
using (ℕ; _≤_; _<_; z≤n; s≤s)
open import Data.Nat.Properties using (≤-refl)
record Range (m n : ℕ) : Set where
constructor bounded
field
value : ℕ
lower : m ≤ value
upper : value < n
SudokuNum : Set
SudokuNum = Range 1 10
one : SudokuNum
one = bounded 1
(s≤s z≤n)
(s≤s (s≤s z≤n))
nine : SudokuNum
nine = bounded 9
(s≤s z≤n)
≤-refl
getValue : ∀ {m n} → Range m n → ℕ
getValue r = Range.value r
bounded 依次接收数字、下界证明和上界证明。上下界 m、n 可以从目标类型推断,无需手动传入。
标准库将 x < n 定义为 suc x ≤ n,所以:
one的上界证明需要证明2 ≤ 10,使用两次s≤s后归结为0 ≤ 8。nine的上界证明需要证明10 ≤ 10,可以直接使用≤-refl。
这些构造器和严格小于关系的定义可见 Data.Nat.Base。
取出数字时使用 Range.value r;需要边界证明时,可以分别使用 Range.lower r 和 Range.upper r,无需再次检查数字是否越界。
同样的定义也允许表示空区间。例如,构造 Range 3 2 的值时,必须同时提供 3 ≤ value 和 value < 2 的证明,而这两个条件无法同时满足。
输入数字时,怎样自动检查范围?
对于程序接收到的任意自然数,可以先判断两条不等式。检查成功时返回携带证明的 Range,失败时返回 nothing。
将下面的代码接在前面的模块中:
open import Data.Nat.Properties using (_≤?_; _<?_)
open import Data.Maybe.Base using (Maybe; just; nothing)
open import Relation.Nullary using (yes; no)
makeRange : (m n x : ℕ) → Maybe (Range m n)
makeRange m n x with m ≤? x | x <? n
... | yes lowerProof | yes upperProof =
just (bounded x lowerProof upperProof)
... | no _ | _ = nothing
... | yes _ | no _ = nothing
readSudokuNum : ℕ → Maybe SudokuNum
readSudokuNum = makeRange 1 10
_≤?_ 和 _<?_ 返回可判定结果。匹配到 yes 时,分支中会得到对应关系的证明,可以直接传给 bounded。Data.Nat.Properties
这个接口可以把外部输入转换成受约束的数字,调用方再通过 just、nothing 处理成功或失败。
原来的 Boolean 版 InRange 是否正确?
在导入所需名称、调整好缩进后,这个定义可以使用:
open import Data.Nat.Base using (_≤ᵇ_; _<ᵇ_)
open import Data.Bool.Base using (T; _∧_)
open import Data.Unit.Base using (tt)
record InRange (m n x : ℕ) : Set where
field
inRange : T ((m ≤ᵇ x) ∧ (x <ᵇ n))
fiveProof : InRange 1 10 5
fiveProof = record { inRange = tt }
T 把 Boolean 结果转换成类型:T true 是有值的类型 ⊤,T false 是空类型 ⊥。Data.Bool.Base
对于 InRange 1 10 5,比较表达式可以计算为 true,所以字段可以填入 tt。对于越界的具体数字,表达式会计算为 false,此时无法提供这个字段的值。
这里的 5 是类型参数,fiveProof 保存的是区间条件的证明。若希望沿用这个谓词,并把数字一起保存,可以使用依赖对:
open import Data.Product.Base using (Σ; _,_)
BoolRange : ℕ → ℕ → Set
BoolRange m n = Σ ℕ (InRange m n)
five : BoolRange 1 10
five = 5 , fiveProof
Σ ℕ (InRange m n) 表示一对相互依赖的值:第一个是 x : ℕ,第二个是 InRange m n x 的证明。这样就把三参数的约束包装成了两参数的数字类型。
若后续经常需要分别使用上下界证明,前面使用 lower、upper 两个字段的写法会更直接。Boolean 写法同样能保证约束,只是在进行关系推理时,可能需要把 Boolean 条件的证明转换成 _≤_、_<_ 的证明。
数独也可以直接使用 Fin 9
如果目标只是表示九种数字,Fin 9 已经足够。它的自然数解释是 0 到 8,显示时加一即可:
open import Data.Fin.Base using (Fin; toℕ)
open import Data.Nat.Base using (suc)
SudokuIndex : Set
SudokuIndex = Fin 9
displayDigit : SudokuIndex → ℕ
displayDigit i = suc (toℕ i)
Fin 及 toℕ 的定义见 Data.Fin.Base。这种表示适合索引和枚举;若希望直接保存自然数,并在类型中明确保留任意上下界,可以使用 Range。
区间内的数字相加或取后继后,结果可能越界。此类操作需要重新检查范围,或者提供结果仍满足边界条件的证明。
备注:内容仅供参考。
