論理推論型のプログラミング言語

市村剛大

 日常世界では論理よりも具体例が身近である。数学の公式を学ぶために、その公式を用いた具体例である設問を解きながら覚えるのだ。むしろ具体例に落とし込めない公式や論理など、日常世界ではなんの役にもたたない。大学受験をする者にとって、大学受験の問題を解くのに使用できない数学の公式は知る必要がないだろう。


 他人の書いたプログラムを読むときの頭の使い方は、数学の問題を解いて公式を理解するときのそれとは真逆に近いものかもしれない。プログラムには(気の利いたコメントがないかぎり)論理しか記述されていない。ある論理を読み、それがシステム上でどう使われるか具体例を想像し、理解する。数学で言うと、ある公式を見せられ、それを使ってどのような問題が解決できるか想像してみる、と言う頭の使い方だろう。何か大学で研究する人は恒常的にこのような頭の使い方をしているのだろうが、日常生活でこのような頭の使い方をすることは滅多にない。


 リーダブルコードという言葉は色々な意味をもつ。プログラムの可読性とはなんだろうか。一つには論理それ自体をどのような論理か、見た目としてわかりやすく解釈できるようにすることだろう。次に人間がその論理がシステム上でどのような意味を持った動作をするか、人間にとってわかりやすくするという意味があるだろう。人間にとって分かりやすいとは何か。一つのアプローチとして、日常生活と同じく具体例ベースでプログラムを記載すれば良いのではないだろうか。


 最近仕事でテストコードを書くことが増えた。テスト駆動開発は柔軟性を持った品質の高いソフトウェアを生み出す手法として優れていることを実感している。テストコードを書く開発者は、それが自然言語における具体例であることを実感したことがあるだろう。ただ、テストコードはテスト駆動開発を行なっていない時には、無駄な作業にも思える。プログラマは自分のコードは正しいと思ってプログラムを書くし、時間がない時はテストコードを書くことによって工数が増えてしまうことを嫌い、疎かにするだろう。であればこのテストコードこそがプログラムの本質、といったようなプログラム言語を作ってしまえば良いのではないだろうか。つまり


 テストコードだけ書いたら勝手にプログラム本体ができあがってくんないかな


 ということだ。これらを踏まえたプログラミング言語はどのような見た目になるのだろうか。具体的なアイデアをラフに書いてみよう。足し算を行うプログラムを考えてみる。



// 整数の足し算を行う

// 普通のプログラム(javascript)

function sum(x, y) {
    return x + y;
}

// 提案するプログラム

function sum{
    when (x = 1, y = 1)  return 2;
    when (x = 0, y = 0)  return 0;
}
        

コンパイラは`1+1=2`などの具体例から、`x + y` というロジックを推論する。また、引数であるxy 、そして返り値が整数型であることも推論する(javascriptの方はnumber型だろうが、、)。この具体例から足し算が推論されていることが不安になる、ということはにひとまず置いておこう。これはブレインストーミングだ。when句を2つ書いたことには理由がある。`when (x = 1, y = 1) return 2;` だけでは例えば`2x - y` という式も推論できてしまうだろう。2変数であれば2点が置かれることで線型な変換については正確に推論できるだろう。



// 整数x10以上ならtrue, 10未満ならfalseを返す

// 普通のプログラム(javascript)

function gteTen(x) {
    if(x >= 10) {
        return true;
    }
    return false;
}

// 提案するプログラム

function gteTen {
    when (x = 11) return true;
    when (x = 10) return true;
    when (x = 9) return false;
}
        

 次の例は`整数x10以上ならtrueを、10未満ならfalseを返す` だ。いわゆる境界値テストになっている。ただ「整数」に入力を絞っているのが苦しい。少数も許容した場合9.9はちゃんとfalseになるのだろうか?テストケースに境界値を表す何かを定義する必要があるだろう。



// 実数xが10以上ならtrue, 10未満ならfalseを返す

function gteTen {
    when (x = 11.0) return true;
    *when (x = 10.0) return true;
    when (x = 9.0) return false;
}
        

とりあえずアスタリスクをつけることで解決だ。ここまでで、おお、と思った人はあまり数学の素養がないだろう。肝心なのは非線形な問題にどう対処するかということだ。テスト駆動的なアプローチをなるべく素直に行いたい。が、複雑になりそうだ、一旦、自分の思っている、最終的なこの言語でのプログラミングの進め方のイメージを具体的に書き出してみる。FizzBuzz問題を例とろう。


FizzBuzz問題:ある整数について3で割り切れる時は文字列「Fizz」を、5で割り切れる時は文字列「Buzzz」を、両方で割り切れる時は「FizzBuzz」を出力する


まず最初に以下のようなコードを書く。



// FizzBuzz問題

function fizzBuzz {
    when(x = 3) return 'Fizz';
    when(x = 5) return 'Buzz';
    when(x = 15) return 'FizzBuzz';
}
        

ここまでで、この言語はxには整数が入るということと、少なくとも上記3パターンにどういう値が返せれば良いか、ということを推論できる。ここでコンパイルを行うと次のような3つのエラーが出る。


'Fizz'が出力されるパターンが他にもあるならばもう一つ書いてください
'Buzz'が出力されるパターンが他にもあるならばもう一つ書いてください
'FizzBuzz'が出力されるパターンが他にもあるならばもう一つ書いてください


そこで、次のようにケースを書き足す。



// FizzBuzz問題

function fizzBuzz {
    when(x = 3) return 'Fizz';
    when(x = 6) return 'Fizz';
    when(x = 5) return 'Buzz';
    when(x = 10) return 'Buzz';
    when(x = 15) return 'FizzBuzz';
    when(x = 30) return 'FizzBuzz';
}
        

ここまで書くと、FizzBuzzのロジックが推論できる。しかし、3の倍数でも5の倍数でもないパターンが推論できない。以下のエラーが出る。


x = 1のとき、どのような値が返却されますか?


そこで次のように書き足すとコンパイルが通るようになる。



// FizzBuzz問題

function fizzBuzz {
    when(x = 1) return 'Fizz';
    when(x = 3) return 'Fizz';
    when(x = 6) return 'Fizz';
    when(x = 5) return 'Buzz';
    when(x = 10) return 'Buzz';
    when(x = 15) return 'FizzBuzz';
    when(x = 30) return 'FizzBuzz';
}
        

 ここまで書けばコンパイルするとちゃんとFizzBuzz問題が解ける関数が推論されていて欲しい。こんなイメージだ。微妙だろうか。「推論」などと呼んでいるが、問題が複雑になるにつれ、このブラックボックスが行なっていることは、コンパイルではなくニューラルネットワークが行う学習に近いものとなるだろう。最終的には人間はコンパイラの推論を手助けするヒントを与え、コンパイラが自信を持って推論できるまでテストケースを提供し続ける作業をすることとなる。線型論理推論の組み合わせで近似できるようになった時点で一旦このプログラムは完成し、バグが発生した際、新しいケースと期待値を入力するとまた作業が始まる。


 うまい感じで関数も分割されて欲しいが、具体的なロジックが思いつかない。ぽっと浮かび上がってきたアイデアだったのだが、形にしてみるとなかなか難しい。結局全パターンを書かなければいけなくなってしまうのではないか、、、テストコードについての課題意識については、確かに自分が感じていることなので間違ってはいないと思う。暇な時にまた続きを考えてみたい。

目次へ