読者です 読者をやめる 読者になる 読者になる

依存型の表現力と逆問題

Idrisみたいに、函数プログラミングを前提にしている言語には函数を合成する演算が標準で用意されているよね。

(.) : (b -> c) -> (a -> b) -> a -> c

この演算子を使うと、函数を合成してくれる。函数の合成を使えば、プログラムを変数を使わないで書くことができる。

整数を文字列にする : Integer -> String
整数を文字列にする = show

最初の文字を得る : String -> Char
最初の文字を得る = strHead

整数を文字列にして最初の文字を得る : Integer -> Char
整数を文字列にして最初の文字を得る = 最初の文字を得る . 整数を文字列にする

Prologよりも簡単だね。

でも、プログラミングでは、引数を複数持つ函数を使うことはよくあることだ。そして、函数プログラミングを前提にしている言語では、そういう函数は普通カリー化された状態で定義されている。

整数を足し合わせる : Integer -> Integer -> Integer
整数を足し合わせる = (+)

これをくっつけたい。するとどうなるか。

整数を足し合わせて文字列にして最初の文字を得る : Integer -> Integer -> Char
整数を足し合わせて文字列にして最初の文字を得る
    = 最初の文字を得る . 整数を文字列にする . 整数を足し合わせる

これは動かない! コンパイルしようとすると説教される。

Moji.idr:17:Can't unify Integer -> Integer -> Integer with Integer -> Integer

Specifically:
	Can't unify Integer -> Integer with Integer

本当に欲しかった演算はこんなのだ。

(>>>) : function a b -> (b -> c) -> function a c

ここで、functionは次のように定義してある。

function : List Type -> Type -> Type
function [] u = u
function (t :: ts) u = t -> function ts u

最初の引数は「引数の型のリスト」で、2番目の引数は「返り値の型」。つまり、 function は、引数の数を限定しない、一般的な函数を表現している。

この演算 (>>>) は実際に定義することができる。

infixl 1 >>>

(>>>) : function a b -> (b -> c) -> function a c
(>>>) {a = Nil} f g = g f
(>>>) {a = t :: ts} f g = \x => f x >>> g

じゃあ、これを使ってプログラムを書いてみよう。

import Function

整数を文字列にする : Integer -> String
整数を文字列にする = show

最初の文字を得る : String -> Char
最初の文字を得る = strHead

整数を文字列にして最初の文字を得る : Integer -> Char
整数を文字列にして最初の文字を得る = 整数を文字列にする >>> 最初の文字を得る

整数を足し合わせる : Integer -> Integer -> Integer
整数を足し合わせる = (+)

整数を足し合わせて文字列にして最初の文字を得る : Integer -> Integer -> Char
整数を足し合わせて文字列にして最初の文字を得る
    = 整数を足し合わせる >>> 整数を文字列にする >>> 最初の文字を得る

しかし、残念な結果が返ってくる。

Moji.idr:10:Can't unify Integer -> String with function a String

Specifically:
	Can't unify Integer -> String with function a String
Moji.idr:17:Can't unify Integer -> Integer -> Integer with function a Integer

Specifically:
	Can't unify Integer -> Integer -> Integer with function a Integer

エラーメッセージは Integer -> String といった型を function a String と unify できなかったということで、つまり a が自動で求められなかったわけだ。

暗黙の引数 a を与えれば、一応動作するようになる。動作するんだけど、コレジャナイ感が強い。

整数を文字列にして最初の文字を得る : Integer -> Char
整数を文字列にして最初の文字を得る
    = (>>>) {a = [Integer]} 整数を文字列にする 最初の文字を得る

整数を足し合わせて文字列にして最初の文字を得る : Integer -> Integer -> Char
整数を足し合わせて文字列にして最初の文字を得る
    = (>>>) {a = [Integer, Integer]}
      ((>>>) {a = [Integer, Integer]} 整数を足し合わせる 整数を文字列にする)
      最初の文字を得る

思えば、Prologはこの逆問題を解くことができる。Prologだと、functionの定義はこうなる。

function([], U, U).
function([T|TS], U, fn(T, X)) :- function(TS, U, X).

すると、次のようにして A を求められる。

?- function(A, string, fn(integer, string)).
A = [integer] ;
false.

?- function(A, string, fn(integer, fn(integer, string))).
A = [integer, integer] ;
false.

Prologの場合は停止しないプログラムも普通に書くことができるという問題があるのだけれども、何らかの改良を加えれば、このような推論は自動で行うことができるような気もする。