#はじめに
Coq/SSReflectで自然数nを引数に取り、n分木の型を返す型を定義します。
子ノード数が不定の一般の木の場合は、前記事「Coq/SSReflectで子ノード数が不定である一般の根付き木をInductiveに定義する」をご覧ください。
#n分木を定義する
まず、n分木がどういうコンストラクタを持てばいいのか、2分木、3分木を見ながら考察していきます。
##2分木や3分木を参考にする
型T
に対する2分木bitree
や3分木tritree
を見ていきましょう。
Variable (T:Type).
Inductive bitree : Type :=
| BiLeaf of T
| BiNode of T & bitree & bitree.
(* BiNode : T -> bitree -> bitree -> bitree *)
Inductive tritree : Type :=
| TriLeaf of T
| TriNode of T & tritree & tritree & tritree.
(* TriNode : T -> tritree -> tritree -> tritree -> tritree *)
どちらも2つのコンストラクタから構成されており、それぞれBiLeaf
、Trileaf
が葉を作るコンストラクタ、BiNode
、TriNode
が枝を作るコンストラクタになっています。
葉を作るコンストラクタBiLeaf
、TriLeaf
はそれぞれT
型の変数をもらって木の型を返すものであり、両者に大きな違いはありませんが、枝を作るコンストラクタBiNode
, TriNode
が大きく異なります。具体的には、どちらもT
型の変数を1つもらうところは同じですが、元の木の型bitree
、tritree
の引数の個数が2個、3個と異なっています。
以上のことを踏まえると、n分木を定義する場合も葉と枝を作るコンストラクタを用意して、枝を作るコンストラクタの引数の個数をn個にすることで定義できそうです。
しかしながら、2分木や3分木と同様にn分木を定義しようとするとうまくいきません。
##2分木や3分木のように定義しようとすると...
2分木や3分木を参考にしながらn分木ntree : nat -> Type -> Type
を定義します。
しかしながら、n分木は自然数nに依存する型となるので、2分木や3分木のようにすんなりと定義させてもらえません。
###失敗例1
n分木の引数n:nat
に対し、葉を作るコンストラクタの型はNLeaf : T -> ntree n
、枝を作るコンストラクの型はNNode : T -> ntree n -> ... -> ntree n -> ntree n
のようになればいいので、以下のように定義してみます。
(* failed *)
Inductive ntree (n:nat) (T:Type) : Type :=
| NLeaf of T
| NNode : T -> iter n (fun X:Type => ntree n T -> X) (ntree n T).
(* Error: The type of constructor nNode is not valid;
its conclusion must be "ntree" applied to its parameter. *)
iter n (fun X => ntree n T -> X) (ntree n T)
の値は確かにntree n T -> ... -> ntree n T -> ntree n T
になるはずですが、Coq側でうまくマッチしてくれないみたいです。
エラー文から察するに、返り値が明確にntree n T
の形で記述されていないとダメなようです。
###失敗例2
そこで、やむなく引数をuncurry化して、返り値がntree n T
型になるようにtupleを引数に取る形に定義し直してみます。
(* failed *)
Inductive ntree (n:nat) (T:Type) : Type :=
| NLeaf of T
| NNode of T & n.-tuple (ntree n T).
(* NNode : T -> n.-tuple (ntree n T) -> ntree n T *)
(* Error: Non strictly positive occurrence of "ntree" in
"T -> n.-tuple (ntree n T) -> ntree n T" *)
ダメみたいです。
###失敗例3
一応、tupleではなく直積の形での記述も試してみますが同様の結果になりました。
(* failed *)
Inductive ntree (n:nat) (T:Type) : Type :=
| NLeaf of T
| NNode of iter n (prod (ntree n T)) T.
(* Error: Non strictly positive occurrence of "ntree" in
"iter n (prod (ntree n T)) T -> ntree n T" *)
n個の引数を直接記述することはできないので、再帰する引数の型をiter
やtuple
のような、何かの関数の引数として与えなくてはなりません。
しかしながら、上記のような記述では通らないため、そもそも関数の返り値内に現れる型で再帰することはCoqでは不可能なのでしょうか?
##一般の木を参考にする
前記事における一般の根付き木の定義をもう一度みてみます。
Variable (T:Type).
Inductive tree : Type := Node of T & seq tree.
一般の根付き木tree
の場合、引数の個数が不定なので、それらをリストseq tree
にまとめ、1つの引数としてコンストラクタNode
に取っています。
ここで、注目するべきはseq tree
で、再帰する型tree
を引数に取っていますが、定義が通ります。
実は、Inductiveに定義された型(この場合はseq
)の引数であれば、再帰する型を取るようなものでも定義できる場合があります。1
そこで、tupleをInductiveに定義しなおし、これを引数に取ることを考えます。
##n分木の型を定義する
改めてn分木を定義していきます。
###n引数を表す型をInductiveに定義する
まず、n個の引数を表す型sizseq
をInductiveに定義します。
Inductive sizseq (T:Type) : nat -> Type :=
| SizNil : sizseq T 0
| SizCons n : T -> (sizseq T n) -> sizseq T n.+1.
sizseq
のnat
型の第二引数が引数の個数を表しており、sizseq T n
と記述したときn
引数のT
型の値を表しています。
具体的な値は、リストのそれと同様にSizCons
とSizNil
を用いて記述します。
Check (SizCons true (SizCons false (SizNil _))).
(* : sizseq bool 2 *)
言い換えれば、sizseq
は長さの証明付きのリストだと考えることができます。
実はn.-tuple
も定義の上では長さの証明付きのリストですが、こちらはリストtval
とsize tval == n
の証明から構成されており、Inductiveに定義されているわけではないため、sizseq
とは構成方法が異なります。
ここで、sizseq (T:Type) (n:nat) : Type
ではなく、sizseq (T:Type) : nat -> Type
と定義しているのは、前者のように外部から与えてしまうと、各コンストラクタの返り値をsizseq T n
に固定しなければならず、n
の値を変更できないためです。実際にはコンストラクタによって異なる値を返さなければならないため、各コンストラクタの引数として後から与える形を取っています。
このように定義しておくと、証明においてs:sizseq T m
に対してelim
またはcase
したとき、それぞれのコンストラクタに対応する各ゴールにおいて、ゴールに存在する全てのm
を対応する値、つまりSizNil
のときは0
、SizCons
のときはn.+1
に書き換えてくれます。言い換えれば、m
は元々n.+1
の形で記述できていたという事実を得ることになります。
ただし、それがm
であったという情報を失うことになることに注意が必要です。
そのため、引数の順番がn分木ntree : nat -> Type -> Type
と異なるからといって、sizseq : nat -> Type -> Type
と定義してしまうと、後述するntree
の定義がうまく通らなくなります。
これは、コンストラクタに戻したときに元の型の情報、つまりntree
型であったという情報を失って、ただの一般の型T
で記述することになってしまい、型が合わなくなるためです。2
Inductive sizseq' : nat -> Type -> Type :=
| SizNil' T : sizseq' 0 T
| SizCons' n T : T -> (sizseq' n T) -> sizseq' n.+1 T.
(* it can be defined, but we can not define n-ary tree using this definition *)
###n分木の型
n分木を先ほどのsizseq
を使って定義します。
Inductive ntree (n:nat) (T:Type) : Type :=
| NLeaf of T
| NNode of T & sizseq (ntree n T) n.
sizseq
がInductiveに定義されているため、この定義が通ります。
使い方としては以下の通りです。
Check (NNode 4 (SizCons (NLeaf _ 5) (SizCons (NLeaf _ 3) (SizNil _)))).
(* : ntree 2 nat *)
これはn=2
のときの要素がnat
型の木で、根の値4であり、値がそれぞれ5と3の葉の子ノードを持っていることを表しています。
ただし、一般の根付き木のときのように帰納原理がうまく自動生成されないので、これを証明する必要があります。
#n分木の帰納原理
n分木の帰納原理を証明する前に、帰納原理を記述するためのsizseq
におけるfold
関数を定義します。
##sizseq
におけるfold関数
sizseq
は基本的には長さの証明付きのリストと考えられるので、fold関数sizfoldr
の定義もリストに関するfoldr
と同様に定義できます。
Fixpoint sizfoldr (T S:Type)
(f:T -> S -> S) (id:S) (n:nat) (i:sizseq T n) : S :=
match i with
| SizNil => id
| SizCons _ x i' => f x (sizfoldr f id i')
end.
##帰納原理
n分木の帰納原理としてほしい命題は以下の通りです。
myntree_ind
: forall (n:nat) (T:Type) (P:ntree n T -> Prop),
(forall x : T, P (NLeaf n x)) ->
(forall (x:T) (s:sizseq (ntree n T) n),
sizfoldr (fun u : ntree n T => and (P u)) True s ->
P (NNode x s)) ->
forall t, P t.
Proof.
葉に対する仮定と枝に対する仮定の2つがあります。
特に枝に関して、sizfoldr (fun u => and (P u)) True s
が全ての子ノードに対して、帰納法の仮定が成り立つことを表しています。
この命題の証明ですが、本記事では直接証明項を記述する方法と、Proof modeで証明する方法の2つを紹介します。
##直接記述する方法
以下のように記述すれば定義できます。
Definition myntree_ind n (T:Type) (P:ntree n T -> Prop)
(Hleaf:forall x, P (NLeaf n x))
(Hnode:forall (x:T) s,
sizfoldr (fun u : ntree n T => and (P u)) True s ->
P (NNode x s)) :
forall t, P t :=
fix loop t : P t :=
match t with
| NLeaf x => Hleaf x
| NNode x s =>
let fix iter_conj (m:nat) (f:sizseq (ntree n T) m) :
sizfoldr (fun u => and (P u)) True f :=
match f with
| SizNil => Logic.I
| SizCons _ y f' => conj (loop y) (iter_conj _ f')
end in
Hnode x s (iter_conj _ s)
end.
基本的には、一般の根付き木のときと同様にfix
文を用いて定義します。
ここで重要なのは、再帰関数loop
内の再帰関数iter_conj
の引数としてm:nat
を取っている点です。このm
はf:sizseq (ntree n T) m
の長さを表していて、再帰するたびにm
が変化するため、引数として取る必要があります。
##proof modeで証明する方法
以下のように証明します。
Lemma myntree_ind' n (T:Type) (P:ntree n T -> Prop) :
(forall x, P (NLeaf n x)) ->
(forall (x:T) s,
sizfoldr (fun u : ntree n T => and (P u)) True s ->
P (NNode x s)) ->
forall t, P t.
Proof.
move => Hleaf Hnode.
fix HP 1.
elim =>[x|x s].
- exact : Hleaf.
- apply : Hnode. move : (ntree n T) P HP s {Hleaf}=> X P HP.
by elim.
Qed.
fix
文を用いるのは一般の根付き木の時と同じですが、ポイントは1つ目のelim
でNNode
に関するゴールにおいてapply : Hnode
をしたあとです。
ここから先は木の性質ではなく、長さ証明付きリストs:sizseq (ntree n T) n
に関する性質を示す場面です。
つまりs
に対して帰納法を使う場面ですが、単にelim : s
としようとすると、前述の通り、ゴールにおける全てのn
が任意のn0
に対するn0.+1
に書き換わることになります。従って型ntree n T
内のn
に関しても同様にn0.+1
に書き変わってしまい、型が元のntree n T
と異なってしまうことから型エラーになってしまうため、そのままではelim
コマンドが通りません。
そこで、一旦ntree n T
型の名前をX
に書き換え(ntree n T
型を含む仮定P
、HP
も全てX
型に書き直します)、n
に依存しない形にしてからelim
を行います。この書き換えによってntree
型であるという情報を失いますが、この時点ですでにntree
型に依存した仮定や関数が存在しないため、このような書き換えを行うことができます。
この操作の意味としては、一般の型X
に対してゴールの性質を示し、それをntree n T
型に適用することであると考えることができます。
#n分木におけるfold関数
次にn分木におけるfold関数を定義します。
fold関数によって、n分木における多くの関数が定義できます。
Definition ntreefold (T S:Type)
(fleaf:T -> S) (fnode:T -> S -> S) (op:S -> S -> S) (id:S) (n:nat)
: ntree n T -> S :=
fix loop t : S :=
match t with
| NLeaf x => fleaf x
| NNode x s =>
let fix iter_op (m:nat) (r:sizseq (ntree n T) m) : S :=
match r with
| SizNil => id
| SizCons k y r' => op (loop y) (iter_op _ r')
end in
fnode x (iter_op _ s)
end.
帰納原理を直接定義した時と同様にfix
文を使って定義します。
リストのfold文と違うのは、葉のときと枝の時で処理する関数が違うところです。
葉と枝で処理を分けることによって、例えば木の高さを計算する関数nheight
も以下のように定義できます。
Definition nheight (T:Type) (n:nat) (t:ntree n T) : nat :=
ntreefold (fun _ => 0) (fun _ => S) maxn 0 t.
ただ、ntreefold
はそのままでは使いづらいので、以下のような等式を示しておきます。
Lemma ntreefold_equal (T S:Type)
(fleaf:T -> S) (fnode:T -> S -> S) (op:S -> S -> S) (id:S)
(n:nat) (t:ntree n T) :
ntreefold fleaf fnode op id t =
match t with
| nLeaf x => fleaf x
| nNode x s =>
fnode x (sizfoldr (fun u => op (ntreefold fleaf fnode op id u)) id s)
end.
Proof.
elim : t =>[|x s]//=. congr(fnode _ _).
move : (ntree n T) (@ntreefold _ _ fleaf fnode op id n) s => X f.
by elim =>[|m i s /= ->].
Qed.
これもproof modeで帰納原理を示したときと同様に、一旦ntree n T
をX
に書き換えてから長さの証明付きリストs
に関する帰納法を使っています。
ここで@ntreefold _ _ fleaf fnode op id n
も書き換えているのは、ntreefold
が木を引数にとる関数であるので、これを関数f
に一般化しないとntree n T
をX
に一般化する際に型エラーを起こすためです。
#まとめ
Coq/SSReflectでn分木を定義し、それにおける帰納原理やfold関数も定義しました。
しかしながらn分木は自然数に依存する型であり、その扱いが難しいため、現実的には一般の根付き木か、素直に2分木や3分木などのように、nを具体化した木の型を個別に定義する方が使いやすいように思えました。