Poly: 多相性と高階関数



Set Warnings "-notation-overridden,-parsing".
From LF Require Export Lists.

ポリモルフィズム(多相性)(Polymorphism)


この章では、関数型プログラミングの基本的なコンセプトを進めていきます。 特に重要な新しいアイディアは、「多相性(polymorphism)」(関数が扱うデータの型に関する抽象化)と「高階関数(higher-order function)」(関数のデータとしての取り扱い)です。 まず多相性から始めましょう。

多相的なリスト


ここまでは、数値のリストについて学んできました。 もちろん、数値以外の、文字列、ブール値、リストといったものを要素として持つリストを扱えるプログラムは、より興味深いものとなるでしょう。 これまで学んできたことだけでも、実は新しい帰納的なデータ型を作ることはできるのです。 例えば次のように...

Inductive boollist : Type :=
  | bool_nil
  | bool_cons (b : bool) (l : boollist).

... しかし、こんなことをやっていると、すぐに嫌になってしまうでしょう。 その理由の一つは、データ型ごとに違ったコンストラクタの名前をつけなければならないことですが、もっと大変なのは、こういったリストを扱う関数(lengthrevなど)を、新しく対応した型ごとに作る必要が出てくることです。

この無駄な繰り返し作業を無くすため、Coqは多相的な帰納的データ型の定義をサポートしています。 これを使うと、多相的なリストは以下のように書くことができます。

Inductive list (X:Type) : Type :=
  | nil
  | cons (x : X) (l : list X).

これは、前の章にあるnatlistの定義とほとんどそっくりですが、コンストラクタconsの引数の型がnatであったのに対し、任意の型を表すXがそれに変わっています。 このXはヘッダに加えられたXと結びつけられ、さらにnatlistであったものがlist Xに置き換わっています。 (ここで、コンストラクタにnilconsといった名前を付けられるのは、最初に定義したnatlistModuleの中で定義されていて、ここからはスコープの外になっているからです。)
それでは、listとはいったい何なのか、ということを正確に考えて見ましょう。 これを考える一つの手がかりは、リストが「型を引数にとり、帰納的な定義を返す関数である」と考えることです。 あるいは「型を引数にとり、型を返す関数」と考えてもいいかもしれません。 任意の型Xについて、list Xという型は、帰納的に定義されたリストの集合で、その要素の型がXとなっているものと考えることもできます。

Check list.

定義中のlistのパラメータXはコンストラクタであるnilconsの引数にもなります。 つまり、nilconsも多相的なコンストラクタになっているということです。 コンストラクタの使用時には、何のリストを作るかを第一引数として与える必要があります。 例えば、nil natnatの空リストを構成します。

Check (nil nat).

Similarly, cons nat adds an element of type nat to a list of type list nat. Here is an example of forming a list containing just the natural number 3.

Check (cons nat 3 (nil nat)).

What might the type of nil be? We can read off the type list X from the definition, but this omits the binding for X which is the parameter to list. Type -> list X does not explain the meaning of X. (X : Type) -> list X comes closer. Coq's notation for this situation is forall X : Type, list X.

Check nil.

Similarly, the type of cons from the definition looks like X -> list X -> list X, but using this convention to explain the meaning of X results in the type forall X, X -> list X -> list X.

Check cons.

(Side note on notation: In .v files, the "forall" quantifier is spelled out in letters. In the generated HTML files and in the way various IDEs show .v files (with certain settings of their display controls), forall is usually typeset as the usual mathematical "upside down A," but you'll still see the spelled-out "forall" in a few places. This is just a quirk of typesetting: there is no difference in meaning.)

リストのコンストラクタに毎回型引数を与えることは無駄な手間だと感じるかもしれません。 すぐにこれを回避する方法について見ていきます。

Check (cons nat 2 (cons nat 1 (nil nat))).

(ここではnilconsを明示的に記述していますが、それは我々がまだ[]::の表記法(notation)をまだ定義していないからです。 この後でやります)

それでは少し話を戻して、以前書いたリスト処理関数を多相版に作り直していきましょう。 repeat関数は以下のようになります。

Fixpoint repeat (X : Type) (x : X) (count : nat) : list X :=
  match count with
  | 0 => nil X
  | S count' => cons X x (repeat X x count')
  end.

As with nil and cons, we can use repeat by applying it first to a type and then to an element of this type (and a number): nilconsと同様に、repeatも型を最初に適用してからその型の要素(そして続いて数)に適用できます。

Example test_repeat1 :
  repeat nat 4 2 = cons nat 4 (cons nat 4 (nil nat)).
Proof. reflexivity. Qed.

このrepeatを別の型のリストに使いたい場合は、適切な型を与えるだけで済みます。

Example test_repeat2 :
  repeat bool false 1 = cons bool false (nil bool).
Proof. reflexivity. Qed.

練習問題: ★★, standard (mumble_grumble)

以下の2つの帰納的に定義された型を考える。

Module MumbleGrumble.

Inductive mumble : Type :=
  | a
  | b (x : mumble) (y : nat)
  | c.

Inductive grumble (X:Type) : Type :=
  | d (m : mumble)
  | e (x : X).

以下の項の中から、型がgrumble Xの形で付けられるものを選びなさい。 (「はい」か「いいえ」を各行に追加しなさい)

型注釈推論


それでは、repeat関数の定義をもう一度書いてみましょう。 ただし今回は、引数の型を指定しないでおきます。 Coqはこれを受け入れてくれるでしょうか?

Fixpoint repeat' X x count : list X :=
  match count with
  | 0 => nil X
  | S count' => cons X x (repeat' X x count')
  end.

うまくいったようです。 Coqがrepeat'にどのような型を設定したのか確認してみましょう。

Check repeat'.
Check repeat.

両者は全く同じ型であることが分かります。 Coqは「型推論(type inference)」という機構を持っていて、これを使い、Xxcountの型が何であるべきかを、その使われ方から導き出します。 例えば、Xconsの最初の引数として使われたことがあれば、consが最初の引数としてTypeを期待しているのでそれに違いありません。 さらにcount0Sとマッチされているので、それはnatに違いない、といった具合です。
このパワフルな機構の存在は、型情報を常にそこら中に書かなければならないわけではない、ということを意味します。 しかし、型を明示的に書くことは、ドキュメントとして、または整合性を確かめるという点で意味があるので、以降もほとんどは型を書いていきます。 これからは自分の書くコードで、型指定を書くところ、書かないところのバランスを考えていく必要があります(多すぎればコードが目障りな型指定で読みにくくなりますし、少なすぎるとプログラムを読む人に型推論させなければならなくなります)。

型引数の構成(Synthesis)


多相的な関数を使うには、通常の引数に加えて型を一つ以上渡さなければなりません。 例えば、repeat関数内での再帰呼び出しには、型Xとして渡されたものをそのまま渡すことになります。 しかしrepeatの二つ目の引数がX型である以上、最初の引数は明らかにX以外あり得ないのではないか、とも考えられます。 それならばなぜわざわざそれを書く必要があるのでしょう。
幸いなことに、Coqではこの種の冗長性を避けることができます。 型を引数に渡すところではどこでも「穴(hole)」_を書くことができるのです。 これは「ここにあるべきものを見つけてください」という意味です。 もう少し正確に言うと、Coqが_を見つけると、手近に集められる情報を集めて「単一化(unify)」します。 集められる情報は、適用されている関数の型や、その適用が現れている文脈から予想される型などですが、それを使って_と置き換えるべき具体的な型を決定するのです。
これを聞くと、型推論と同じじゃないかと思われるかもしれません。 実際、この二つの機能は水面下にあるメカニズムではつながっています。 次のように単に引数の型を省略する代わりに
      repeat' X x count : list X :=
このように、型を_で書くことができるということです。
      repeat' (X : _) (x : _) (count : _) : list X :=
いずれも、Coqに、欠落している情報を推論させるもので、ちょうど引数を構成することに相当します。
穴を使うと、repeat関数は以下のように書けます。

Fixpoint repeat'' X x count : list X :=
  match count with
  | 0 => nil _
  | S count' => cons _ x (repeat'' _ x count')
  end.

この例では、X_に省略することをあまりしていません。 しかし多くの場合、入力の容易さと読みやすさの両方で違いが現れます。 例えば、1,2,3の三つの数値を保持するリストを書きたい場合を考えてみましょう。 以下のように書く代わりに...

Definition list123 :=
  cons nat 1 (cons nat 2 (cons nat 3 (nil nat))).

...穴を使って以下のように書くことができます。

Definition list123' :=
  cons _ 1 (cons _ 2 (cons _ 3 (nil _))).

暗黙の引数


さらに先に進めて、プログラムにほとんど_を書かなくてすむように、特定の関数の引数については「常に」型推論するよう指定できます。
Arguments命令は関数(や構成子)に対し、引数の名前を波括弧で覆うことで暗黙のものとすることができます。 (構成子でよく起こるのですが、もし名前がない変数を暗黙にしたい場合はワイルドカードパターンとして_を指定します。)

Arguments nil {X}.
Arguments cons {X} _ _.
Arguments repeat {X} x count.

Now, we don't have to supply type arguments at all:

Definition list123'' := cons 1 (cons 2 (cons 3 nil)).

また、関数定義の中で、引数を暗黙のものにすることもできます。 その際は、次のように引数を丸括弧の代わりに波括弧で囲みます。

Fixpoint repeat''' {X : Type} (x : X) (count : nat) : list X :=
  match count with
  | 0 => nil
  | S count' => cons x (repeat''' x count')
  end.

(ここで注意してほしいのは、再帰呼び出しのrepeat'''ではもうすでに型を引数で指定していない、ということです。 また、渡そうとするのは誤りとなります。)
これからは、定義時に指定する後者の書き方をできるだけ使っていくことにします。 ただし、Inductiveの構成子ではArgumentsを明示的に書くようにします。 というのも、帰納型のパラメータを暗黙にするのは、型そのもののパラメータを暗黙にするのであって、構成子のパラメータだけを暗黙にするわけではないからです。 例えば、以下のようにlistを定義したとしましょう。

Inductive list' {X:Type} : Type :=
  | nil'
  | cons' (x : X) (l : list').

Because X is declared as implicit for the entire inductive definition including list' itself, we now have to write just list' whether we are talking about lists of numbers or booleans or anything else, rather than list' nat or list' bool or whatever; this is a step too far.
Let's finish by re-implementing a few other standard list functions on our new polymorphic lists...

Fixpoint app {X : Type} (l1 l2 : list X)
             : (list X) :=
  match l1 with
  | nil => l2
  | cons h t => cons h (app t l2)
  end.

Fixpoint rev {X:Type} (l:list X) : list X :=
  match l with
  | nil => nil
  | cons h t => app (rev t) (cons h nil)
  end.

Fixpoint length {X : Type} (l : list X) : nat :=
  match l with
  | nil => 0
  | cons _ l' => S (length l')
  end.

Example test_rev1 :
  rev (cons 1 (cons 2 nil)) = (cons 2 (cons 1 nil)).
Proof. reflexivity. Qed.

Example test_rev2:
  rev (cons true nil) = cons true nil.
Proof. reflexivity. Qed.

Example test_length1: length (cons 1 (cons 2 (cons 3 nil))) = 3.
Proof. reflexivity. Qed.

Supplying Type Arguments Explicitly


引数を暗黙的に宣言することには、小さな問題が一つあります。 時折、Coqが型を特定するために必要な情報を十分に集められない時があるのです。 そういう場合には、その時だけ明示してやります。 例えば、もし次のように書いたとします。

Fail Definition mynil := nil.

Definitionの前にあるFailという修飾子は、実行すると失敗することを表します。 この修飾子はどのコマンドにも使えます。 もし指定したコマンドが失敗すると、Coqがエラーメッセージを出しますが、処理を続行します。)
この定義ではCoqがエラーを出します。 nilにどんな型を与えれば良いかわからないからです。 このようなときは、型宣言を明示的に行うことができます(これによってnilを何に「適用したか」という情報が与えられます)。

Definition mynil : list nat := nil.

それとは別に、関数名の前に@を付けることで暗黙的に宣言された引数を明示的に与えることができます。

Check @nil.

Definition mynil' := @nil nat.

引数の構成と暗黙の引数をつかうことで、リストに関する便利な表記法を定義できます。 コンストラクタの型引数を暗黙的に記述すれば、Coqはその表記法を使ったときに型を自動的に推論してくれます。

Notation "x :: y" := (cons x y)
                     (at level 60, right associativity).
Notation "[ ]" := nil.
Notation "[ x ; .. ; y ]" := (cons x .. (cons y []) ..).
Notation "x ++ y" := (app x y)
                     (at level 60, right associativity).

これで、我々の望む書き方ができるようになりました。

Definition list123''' := [1; 2; 3].

練習問題


練習問題: ★★, standard, optional (poly_exercises)

ここにあるいくつかの練習問題は、List.vにあったものと同じですが、多相性の練習になります。 以下の証明を完成させなさい。

Theorem app_nil_r : forall (X:Type), forall l:list X,
  l ++ [] = l.
Proof.
Admitted.

Theorem app_assoc : forall A (l m n:list A),
  l ++ m ++ n = (l ++ m) ++ n.
Proof.
Admitted.

Lemma app_length : forall (X:Type) (l1 l2 : list X),
  length (l1 ++ l2) = length l1 + length l2.
Proof.
Admitted.

Exercise: 2 stars, standard, optional (more_poly_exercises)

Here are some slightly more interesting ones...

Theorem rev_app_distr: forall X (l1 l2 : list X),
  rev (l1 ++ l2) = rev l2 ++ rev l1.
Proof.
Admitted.

Theorem rev_involutive : forall X : Type, forall l : list X,
  rev (rev l) = l.
Proof.
Admitted.

多相的な対


次も同様に、前の章で作った数値の対を多相的な対に一般化することを考えます。 これは「直積(products)」とも呼ばれます。

Inductive prod (X Y : Type) : Type :=
| pair (x : X) (y : Y).

Arguments pair {X} {Y} _ _.

リストと同様、型引数を暗黙にし、その表記法を定義します。

Notation "( x , y )" := (pair x y).

また、直積「型」の、より標準的な表記法も登録しておきます。

Notation "X * Y" := (prod X Y) : type_scope.

type_scopeというアノテーションは、この省略形が、型を解析する際のみ使われることを示しています。 これによって、*が乗算の演算子と衝突することを避けています。

最初のうちは、(x,y)X*Yの違いに混乱するかもしれません。 覚えてほしいのは、(x,y)が対の「値」を二つの値から構成するもので、X*Yは対の「型」を二つの型から構成するものだということです。 値xX型で、値yY型なら、値(x,y)X*Y型となります。

対の最初の要素、2番目の要素を返す射影関数は他のプログラミング言語で書いたものと大体同じになります。

Definition fst {X Y : Type} (p : X * Y) : X :=
  match p with
  | (x, y) => x
  end.

Definition snd {X Y : Type} (p : X * Y) : Y :=
  match p with
  | (x, y) => y
  end.

次の関数は二つのリストを引数にとり、一つの"対のリスト"を作成します。 他の関数型言語でzip関数と呼ばれているものですが、ここではCoqの標準ライブラリに合わせる形でcombineと呼ぶことにします。

Fixpoint combine {X Y : Type} (lx : list X) (ly : list Y)
           : list (X*Y) :=
  match lx, ly with
  | [], _ => []
  | _, [] => []
  | x :: tx, y :: ty => (x, y) :: (combine tx ty)
  end.

練習問題: ★, standard, optional (combine_checks)

次の質問の答えを紙に書いた後で、Coqの出した答えと同じかチェックしなさい。

練習問題: ★★, standard, recommended (split)

split関数はcombineと全く逆で、ペアのリストを引数に受け取り、リストのペアを返します。 多くの関数型言語でunzipと呼ばれているものです。
split関数の定義を完成させ、続くテストを通過することも確認しなさい。

Fixpoint split {X Y : Type} (l : list (X*Y))
               : (list X) * (list Y)
  . Admitted.

Example test_split:
  split [(1,false);(2,false)] = ([1;2],[false;false]).
Proof.
Admitted.

多相的なオプション


最後に、多相的なオプションに取り組みます。 この型は、前の章のnatoptionを一般化したものになります。 (optionは標準ライブラリで定義されていて、後ほどそちらを使いたいので、ここでは定義をモジュール内に入れています。)

Module OptionPlayground.

Inductive option (X:Type) : Type :=
  | Some (x : X)
  | None.

Arguments Some {X} _.
Arguments None {X}.

End OptionPlayground.

nth_error関数を、色々な型のリストで使えるように定義し直しましょう。

Fixpoint nth_error {X : Type} (l : list X) (n : nat)
                   : option X :=
  match l with
  | [] => None
  | a :: l' => if n =? O then Some a else nth_error l' (pred n)
  end.

Example test_nth_error1 : nth_error [4;5;6;7] 0 = Some 4.
Proof. reflexivity. Qed.
Example test_nth_error2 : nth_error [[1];[2]] 1 = Some [2].
Proof. reflexivity. Qed.
Example test_nth_error3 : nth_error [true] 2 = None.
Proof. reflexivity. Qed.

練習問題: ★, standard, optional (hd_error_poly)

前の章に出てきたhd_error関数の多相版を定義しなさい。 次の単体テストでの確認も忘れずに。

Definition hd_error {X : Type} (l : list X) : option X
  . Admitted.

再び、暗黙の引数を明示してみましょう。 関数名の前に@をつければいいのでしたね。

Check @hd_error.

Example test_hd_error1 : hd_error [1;2] = Some 1.
Admitted.
Example test_hd_error2 : hd_error [[1];[2]] = Some [1].
Admitted.

データとしての関数


他の多くの近代的なプログラミング言語(MLやHaskell、Scheme、Scala、Clojureなどの関数型言語を含む)同様、Coqは関数を第1級に属するものとして取り扱います。 第1級に属するということは、他の関数の引数として渡したり、結果として返したり、データ構造の中に含めたりできる、ということです。

高階関数


関数を操作する関数を、一般に「高階(higher-order)」関数と呼びます。 例えば次のようなものです。

Definition doit3times {X:Type} (f:X->X) (n:X) : X :=
  f (f (f n)).

ここで引数fは(XからXへの)関数です。 doit3timesの内容は、値nに対して関数fを3回適用するものです。

Check @doit3times.

Example test_doit3times: doit3times minustwo 9 = 3.
Proof. reflexivity. Qed.

Example test_doit3times': doit3times negb true = false.
Proof. reflexivity. Qed.

フィルター(Filter)関数


次に紹介するのは、もっと有用な高階関数です。 これは、X型のリストと、Xについての述語(Xを引数にとり、boolを返す関数)を引数にとり、リストの要素に「フィルター(filter)」をかけて、述語の結果がtrueとなった要素だけのリストを返す関数です。

Fixpoint filter {X:Type} (test: X->bool) (l:list X)
                : (list X) :=
  match l with
  | [] => []
  | h :: t => if test h then h :: (filter test t)
                        else filter test t
  end.

例えば、このfilter関数に述語としてevenbと数値のリストlを与えると、リストlの要素のうち偶数の要素だけが含まれるリストとなって返ります。

Example test_filter1: filter evenb [1;2;3;4] = [2;4].
Proof. reflexivity. Qed.

Definition length_is_1 {X : Type} (l : list X) : bool :=
  (length l) =? 1.

Example test_filter2:
    filter length_is_1
           [ [1; 2]; [3]; [4]; [5;6;7]; []; [8] ]
  = [ [3]; [4]; [8] ].
Proof. reflexivity. Qed.

このfilterを使うことで、 Lists.vにあるcountoddmembers関数をもっと簡潔に書くことができます。

Definition countoddmembers' (l:list nat) : nat :=
  length (filter oddb l).

Example test_countoddmembers'1: countoddmembers' [1;0;3;1;4;5] = 4.
Proof. reflexivity. Qed.
Example test_countoddmembers'2: countoddmembers' [0;2;4] = 0.
Proof. reflexivity. Qed.
Example test_countoddmembers'3: countoddmembers' nil = 0.
Proof. reflexivity. Qed.

無名(匿名)関数


少し悲しいことに、上の例では、filterの引数とするために、多分以降全く使われないであろう関数length_is_1をわざわざ定義して名付けなければなりませんでした。 このようなことは、この例だけの問題ではありません。 高階関数を使うときは、引数として以降では全く使わない「一度きり」の関数を渡すことがよくあります。 こういう関数にいちいち名前を考えるのは、退屈以外の何物でもありません。
幸いなことに、いい方法があります。 関数を、トップレベルで宣言することも名前を付けることもなく、「その場で(on the fly)」作ることができるのです。

Example test_anon_fun':
  doit3times (fun n => n * n) 2 = 256.
Proof. reflexivity. Qed.

The expression (fun n => n * n) can be read as "the function that, given a number n, yields n * n."

次はfilterの例を、無名関数を使って書き換えた例です。

Example test_filter2':
    filter (fun l => (length l) =? 1)
           [ [1; 2]; [3]; [4]; [5;6;7]; []; [8] ]
  = [ [3]; [4]; [8] ].
Proof. reflexivity. Qed.

練習問題: ★★, standard (filter_even_gt7)

Fixpointの代わりにfilter関数を使い、数値のリストを入力すると、そのうち偶数でなおかつ7より大きい要素だけを取り出すfilter_even_gt7関数を書きなさい。

Definition filter_even_gt7 (l : list nat) : list nat
  . Admitted.

Example test_filter_even_gt7_1 :
  filter_even_gt7 [1;2;6;9;10;3;12;8] = [10;12;8].
Admitted.

Example test_filter_even_gt7_2 :
  filter_even_gt7 [5;2;6;19;129] = [].
Admitted.

練習問題: ★★★, standard (partition)

filter関数を使って、partition関数を書きなさい。
      partition : forall X : Type,
                  (X -> bool) -> list X -> list X * list X
partition関数は、型XXの値がある条件を満たすかを調べる述語(X -> bool型の関数)とlist Xの値を引数に与えると、リストの対を返します。 対の最初の要素は、与えられたリストのうち、条件を満たす要素だけのリストで、二番目の要素は、条件を満たさないもののリストです。 二つのリストの要素の順番は、元のリストの順と同じでなければなりません。

Definition partition {X : Type}
                     (test : X -> bool)
                     (l : list X)
                   : list X * list X
  . Admitted.

Example test_partition1: partition oddb [1;2;3;4;5] = ([1;3;5], [2;4]).
Admitted.
Example test_partition2: partition (fun x => false) [5;9;0] = ([], [5;9;0]).
Admitted.

マップ(Map)関数


もう一つ、便利な高階関数としてmapを挙げます。

Fixpoint map {X Y: Type} (f:X->Y) (l:list X) : (list Y) :=
  match l with
  | [] => []
  | h :: t => (f h) :: (map f t)
  end.

これは、関数fとリスト l = [n1, n2, n3, ...] を引数にとり、関数flの各要素に適用した [f n1, f n2, f n3,...] というリストを返します。 例えばこのようなものです。

Example test_map1: map (fun x => plus 3 x) [2;0;2] = [5;3;5].
Proof. reflexivity. Qed.

入力されるリストの要素の型と、出力されるリストの要素の型は同じである必要はありません。 mapは、2つの型変数XYを取ります。 これにより、次の例では、数値のリストと、数値を受け取りbool値を返す関数から、bool型のリストを返しています。

Example test_map2:
  map oddb [2;1;2;5] = [false;true;false;true].
Proof. reflexivity. Qed.

mapは、数値のリストと、「数値からbool型のリストへの関数」を引数にとることで、「bool型のリストのリスト」を返すこともできます。

Example test_map3:
    map (fun n => [evenb n;oddb n]) [2;1;2;5]
  = [[true;false];[false;true];[true;false];[false;true]].
Proof. reflexivity. Qed.

Exercises


練習問題: ★★★, standard (map_rev)

maprevが可換であることを示しなさい。 補題をたてて証明する必要があるでしょう。

Theorem map_rev : forall (X Y : Type) (f : X -> Y) (l : list X),
  map f (rev l) = rev (map f l).
Proof.
Admitted.

練習問題: ★★, standard, recommended (flat_map)

map関数は、list Xからlist Yへのマップを、型X -> Yの関数を使って実現しています。 同じような関数flat_mapを定義しましょう。 これはlist Xからlist Yへのマップですが、X -> list Yとなる関数fを使用します。 このため、次のように要素ごとの関数fの結果を平坦化してやる必要があります。
        flat_map (fun n => [n;n+1;n+2]) [1;5;10]
      = [1; 2; 3; 5; 6; 7; 10; 11; 12]

Fixpoint flat_map {X Y: Type} (f: X -> list Y) (l: list X)
                   : (list Y)
  . Admitted.

Example test_flat_map1:
  flat_map (fun n => [n;n;n]) [1;5;4]
  = [1; 1; 1; 5; 5; 5; 4; 4; 4].
Admitted.

リスト以外にもmap関数が定義できるような帰納型はあります。 次の定義は、option型のためにmap関数を定義したものです。

Definition option_map {X Y : Type} (f : X -> Y) (xo : option X)
                      : option Y :=
  match xo with
    | None => None
    | Some x => Some (f x)
  end.

練習問題: ★★, standard, optional (implicit_args)

filtermap関数を定義したり使ったりするケースでは、多くの場合暗黙的な型引数が使われます。 暗黙の型引数定義に使われている波括弧を丸括弧に置き換え、必要なところに型引数を明示的に書くようにして、それが正しいかどうかをCoqでチェックしなさい。 (この練習問題はここに書かないようにしましょう。 あとで簡単に戻せるように、このファイルをコピーして編集し、終わった後捨てるのが一番楽だと思います。)

畳み込み(Fold)関数


さらにパワフルな高階関数foldに話を移します。 この関数はGoogleの分散フレームワーク「map/reduce」でいうところの「reduce」操作の元になったものです。

Fixpoint fold {X Y: Type} (f: X->Y->Y) (l: list X) (b: Y)
                         : Y :=
  match l with
  | nil => b
  | h :: t => f h (fold f t b)
  end.

直観的にfoldは、与えられたリストの各要素の間に、与えられた二項演算子fを挟み込むように挿入していくような操作です。 例えば、 fold plus [1,2,3,4] は、直感的に1+2+3+4と同じ意味です。 ただし正確には、二番めの引数に、fに最初に与える"初期値"が必要になります。例えば
       fold plus [1;2;3;4] 0
は、次のように解釈されます。
       1 + (2 + (3 + (4 + 0)))
もう少し例を見ていきましょう。

Check (fold andb).

Example fold_example1 :
  fold mult [1;2;3;4] 1 = 24.
Proof. reflexivity. Qed.

Example fold_example2 :
  fold andb [true;true;false;true] true = false.
Proof. reflexivity. Qed.

Example fold_example3 :
  fold app [[1];[];[2;3];[4]] [] = [1;2;3;4].
Proof. reflexivity. Qed.

練習問題: ★, advanced (fold_types_different)

fold関数がXY2つの型引数をとっていて、関数fXYの値を引数にとりYを返すようになっていることに注目してください。 XYが別々の型となっていることで、便利になるような場面を考えなさい。

関数を構築する関数


これまで見てきた高階関数は、ほとんどが関数を引数にとるものでした。 ここでは、関数が別の関数の戻り値になるような例を見ていきましょう。 まず、(型Xの)値xを引数としてとり、「nat型の引数からX型の戻り値を返す関数」を返す関数を考えます。 返す関数は、与えられたnat型の引数に関係なく、生成時に与えられた値xを常に返すものです。

Definition constfun {X: Type} (x: X) : nat->X :=
  fun (k:nat) => x.

Definition ftrue := constfun true.

Example constfun_example1 : ftrue 0 = true.
Proof. reflexivity. Qed.

Example constfun_example2 : (constfun 5) 99 = 5.
Proof. reflexivity. Qed.

実は、我々がすでに見た、複数の引数を持つ関数は、関数をデータとして渡すサンプルになっているのです。 どういうことかは、plus関数の型を思い出せば分かります。

Check plus.

->は、型同士の二項演算子です。 さらに、この演算子は右結合であるため、plus関数の型はnat -> (nat -> nat)の省略です。 これをよく読むと「plusnat型の引数を一つ取り、引数がnat型一つでnat型を返す関数を返す」となります。 以前の例ではplusに二つの引数を一緒に与えていましたが、もし望むなら最初の一つだけを与えることもできます。 これを「部分適用(partial application)」といいます。

Definition plus3 := plus 3.
Check plus3.

Example test_plus3 : plus3 4 = 7.
Proof. reflexivity. Qed.
Example test_plus3' : doit3times plus3 0 = 9.
Proof. reflexivity. Qed.
Example test_plus3'' : doit3times (plus 3) 0 = 9.
Proof. reflexivity. Qed.

さらなる練習問題


Module Exercises.

練習問題: ★★, standard (fold_length)

リストに関する多くの一般的な関数はfoldを使って実装できます。 例えば、次に示すのはlengthの別な実装です。

Definition fold_length {X : Type} (l : list X) : nat :=
  fold (fun _ n => S n) l 0.

Example test_fold_length1 : fold_length [4;7;0] = 3.
Proof. reflexivity. Qed.

fold_lengthが正しいことを証明しなさい。 (ヒント:reflexivitysimplよりも簡単化を進めます。 そのため、simplでは何も起こらないのにreflexivityが証明してしまうようなゴールもありえます。)

Theorem fold_length_correct : forall X (l : list X),
  fold_length l = length l.
Proof.
Admitted.

練習問題: ★★★, standard (fold_map)

map関数もfoldを使って書くことができます。 以下のfold_mapを完成させなさい。

Definition fold_map {X Y: Type} (f: X -> Y) (l: list X) : list Y
  . Admitted.

fold_mapの正しさを示す定理をCoqで書き、証明しなさい。 (ヒント・再掲:reflexivitysimplよりも簡単化を進めます。)

練習問題: ★★, advanced (currying)

Coqでは、f : A -> B -> Cという関数の型はA -> (B -> C)型と同じです。 このことは、もし関数fAの値を与えると、f' : B -> Cという型の関数が戻り値として返ってくるということです。 さらに、f'Bの値を与えるとCの値が返ってきます。 これは部分適用のplus3でやった通りです。 このように、複数の引数を、関数を返す関数を通じて処理することを、論理学者ハスケル・カリーにちなんで「カリー化(currying)」と呼んでいます。
逆に、A -> B -> C型の関数を(A * B) -> C型の関数に変換することもできます。 これを「アンカリー化(uncurrying)」といいます。 アンカリー化された二項演算は、二つの引数を同時に対の形で与える必要があり、部分適用はできません。

カリー化は以下のように定義できます。

Definition prod_curry {X Y Z : Type}
  (f : X * Y -> Z) (x : X) (y : Y) : Z := f (x, y).

練習問題として、その逆のprod_uncurryを定義し、二つの関数が互いに逆関数であることを証明しなさい。

Definition prod_uncurry {X Y Z : Type}
  (f : X -> Y -> Z) (p : X * Y) : Z
  . Admitted.

As a (trivial) example of the usefulness of currying, we can use it to shorten one of the examples that we saw above:

Example test_map1': map (plus 3) [2;0;2] = [5;3;5].
Proof. reflexivity. Qed.

考える練習: 次のコマンドを実行する前に、prod_curryprod_uncurryの型を考えなさい。

Check @prod_curry.
Check @prod_uncurry.

Theorem uncurry_curry : forall (X Y Z : Type)
                        (f : X -> Y -> Z)
                        x y,
  prod_curry (prod_uncurry f) x y = f x y.
Proof.
Admitted.

Theorem curry_uncurry : forall (X Y Z : Type)
                        (f : (X * Y) -> Z) (p : X * Y),
  prod_uncurry (prod_curry f) p = f p.
Proof.
Admitted.

練習問題: ★★, advanced (nth_error_informal)

nth_error関数の定義を思い出してください。
   Fixpoint nth_error {X : Type} (l : list X) (n : nat) : option X :=
     match l with
     | [] => None
     | a :: l' => if n =? O then Some a else nth_error l' (pred n)
     end.
次の定理の、非形式的な証明を書きなさい。
   forall X n l, length l = n -> @nth_error X l n = None

以降の練習問題では、自然数を定義する他の方法として、「チャーチ数(Church numerals)」と呼ばれる、数学者アロンゾ・チャーチ(Alonzo Church)にちなむ方法を見ていきます。 自然数nを、関数fを取りfn回繰り返し適用するものとして表現します。

Module Church.
Definition cnat := forall X : Type, (X -> X) -> X -> X.

ではこの方法でどのように数値を表すのか見ていきます。 fを1回だけ適用するというのは、単に適用するだけです。 つまり、次のようになります。

Definition one : cnat :=
  fun (X : Type) (f : X -> X) (x : X) => f x.

同様に、twoは引数に2回fを適用するものです。

Definition two : cnat :=
  fun (X : Type) (f : X -> X) (x : X) => f (f x).

zeroの定義は少し変わっています。 0回関数を適用するとは一体どういうことでしょうか? 答えは簡単です:単に引数に何もしなければいいのです。

Definition zero : cnat :=
  fun (X : Type) (f : X -> X) (x : X) => x.

一般に、数値nfun X f x => f (f ... (f x) ...)のような形で、fn回出現するものになります。 つまり、以前定義したdoit3times関数は実はちょうどチャーチ数での3を表していることになります。

Definition three : cnat := @doit3times.

以下の関数の定義を完成させなさい。 また、reflexivityを使えば証明できる以下の単体テストを通過することを確かめなさい。

練習問題: ★, advanced (church_succ)


自然数の+1:チャーチ数nについて、succ nn回とさらにもう一回繰り返す関数です。
Definition succ (n : cnat) : cnat
  . Admitted.

Example succ_1 : succ zero = one.
Proof. Admitted.

Example succ_2 : succ one = two.
Proof. Admitted.

Example succ_3 : succ two = three.
Proof. Admitted.


練習問題: ★, advanced (church_plus)


二つの自然数の加算:
Definition plus (n m : cnat) : cnat
  . Admitted.

Example plus_1 : plus zero one = one.
Proof. Admitted.

Example plus_2 : plus two three = plus three two.
Proof. Admitted.

Example plus_3 :
  plus (plus two two) three = plus one (plus three three).
Proof. Admitted.


練習問題: ★★, advanced (church_mult)


乗算:
Definition mult (n m : cnat) : cnat
  . Admitted.

Example mult_1 : mult one one = one.
Proof. Admitted.

Example mult_2 : mult zero (plus three three) = zero.
Proof. Admitted.

Example mult_3 : mult two three = plus three three.
Proof. Admitted.


練習問題: ★★, advanced (church_exp)


冪:

(ヒント:ここでは多相性が非常に重要な役割を果たします。 しかし、適切な型を指定するのは結構面倒です。 もし"Universe inconsistency"というエラーに遭遇した場合、他の型を試してみてください。 cnatに関してcnatで繰り返すのは問題を起こすものなのです。)

Definition exp (n m : cnat) : cnat
  . Admitted.

Example exp_1 : exp two two = plus two two.
Proof. Admitted.

Example exp_2 : exp three zero = one.
Proof. Admitted.

Example exp_3 : exp three two = plus (mult two (mult two two)) one.
Proof. Admitted.


End Church.

End Exercises.