古き良きAIを勉強中です。
PAIPの2章で紹介されている簡単なルールで英文を作成するプログラムをRustで書きました。
https://github.com/norvig/paip-lisp/blob/master/docs/chapter2.md#23-a-rule-based-solution
グローバル変数の初期化と、それにアクセスする関数定義まで、Rustのマクロを使って書いたのでまとめました。
A Rule-Based Solution
プログラムの内容。
(defparameter *simple-grammar*
'((sentence -> (noun-phrase verb-phrase))
(noun-phrase -> (Article Noun))
(verb-phrase -> (Verb noun-phrase))
(Article -> the a)
(Noun -> man ball woman table)
(Verb -> hit took saw liked))
"A grammar for a trivial subset of English.")
このように簡単な英語の文法をListで表現し、そこから英文を生成します。
Listの中の -> はみやすくしているだけで意味はありません。->の左が名刺や動詞といったルールの名前を示しています。->の先に単語(シンボル)が並んでいる場合はそこから一つ選びます。Articleならtheかaのどちらかがランダムで選択されます。->の右がListの場合は、それらをつなげて英文にしていきます。1991年に書かれたAIの本の最初の最初の例題なので、アルゴリズムよりもこういう形で書くとCommon Lispで上手く扱える事に注目したいところです。
Rustの実装
ルールの部分をマクロを使ってそれっぽくしました。いくつかの単語から選ぶルールです。
(Article -> the a)
(Noun -> man ball woman table)
(Verb -> hit took saw liked))
これがこう書けます。
simple_rule!(article, ->, "a the");
simple_rule!(noun, ->, "man ball woman table");
simple_rule!(verb, ->, "hit took saw liked");
lazy_static!
(Article -> the a)を、["a", "the"] のように文字列の配列にして、グローバル変数にしました。一回だけ初期化するグローバル変数を作るのにlazy_static!が使えました。初期化したいグローバル変数をlazy_static!{}で囲んで、初期化ルーチンを書きます。gensymは次に説明します。
lazy_static! {
static ref $gensym:Vec<&'static str> = {
let mut table:Vec<&'static str> = Vec::new();
for s in $e3.split(' ') {
table.push(s);
}
table
};
}
gensym!
ルールが2つ以上になると、上で作ろうとしているグローバル変数名が被るので、gynsym!を使って名前がぶつからないようにしています。
gensym!は、このようにマクロ自体を gensym::gensym! {} でくくると、マクロの中身に$gensymとして値を渡せるようです。
macro_rules! simple_rule {
($e1: ident, $e11:tt, $e2: expr) => {
gensym::gensym! {
_simple_rule!{ $e1, $e11, $e2}
}
};
}
マクロ本体(_simple_rule!)では、$gensymとして、変数名にアクセスできました。
fn $e1()-> Vec<&'static str> {
vec![$gensym[random_elm_idx($gensym.len())]]
}
https://docs.rs/gensym/0.1.0/gensym/
gensymの情報はこれくらいしか見つからず、他に注意点がないのか気になります。
不要な引数 ->
見やすいために入っているだけのシンボル->については、マクロの引数の型にttを指定するとエラーなく処理できました。
($gensym:ident, $e1: ident, $e2:tt, $e3: expr) => {
だいだいこれだけやれば、後は普通のマクロを書けばOKです。
出来上がったルール
結局上のListで書かれたルールを、こう書くことができました。
composite_rule!(noun_phrase, ->, article, noun);
composite_rule!(verb_phrase, ->, verb, noun_phrase);
composite_rule!(sentence, ->, noun_phrase, verb_phrase);
simple_rule!(article, ->, "a the");
simple_rule!(noun, ->, "man ball woman table");
simple_rule!(verb, ->, "hit took saw liked");
これで、関数を6つ、グローバル変数を3つ作っています。Rustのマクロすごい。
実行すると、頭悪そうな英文を無限に作ってくれます。
a table took the table .
a table liked a ball .
a table saw a man .
the table took a man .
a ball liked a table .
a man liked a woman .
a table hit a ball .
the man liked a man .
the table hit the man .
a woman liked the ball .
ソース
以下、全ソースです。
//https://github.com/norvig/paip-lisp/blob/master/docs/chapter2.md#23-a-rule-based-solution
use rand::Rng;
#[macro_use]
extern crate lazy_static;
extern crate gensym;
fn random_elm_idx(i:usize) -> usize {
let mut rng = rand::thread_rng();
rng.gen_range(0, i as u32) as usize
}
macro_rules! _simple_rule {
($gensym:ident, $e1: ident, $e2:tt, $e3: expr) => {
lazy_static! {
static ref $gensym:Vec<&'static str> = {
let mut table:Vec<&'static str> = Vec::new();
for s in $e3.split(' ') {
table.push(s);
}
table
};
}
fn $e1()-> Vec<&'static str> {
vec![$gensym[random_elm_idx($gensym.len())]]
}
}
}
macro_rules! simple_rule {
($e1: ident, $e11:tt, $e2: expr) => {
gensym::gensym! {
_simple_rule!{ $e1, $e11, $e2}
}
};
}
macro_rules! composite_rule {
($e1: ident, $e2:tt, $e3: expr, $e4: expr) => {
fn $e1() -> Vec<&'static str> {
let mut n:Vec<&str> = Vec::new();
for s in $e3() {
n.push(s);
}
for s in $e4() {
n.push(s);
}
n
}
};
}
/*
(defparameter *simple-grammer*
'((sentence -> (noun-phrase verb-phrase))
(noun-phrase -> (Article Noun))
(verb-phrase -> (Verb noun-phrase))
(Article -> the a)
(Noun -> man ball woman table)
(Verb -> hit took saw liked))
"A grammar for a trivial subset of English")
*/
composite_rule!(noun_phrase, ->, article, noun);
composite_rule!(verb_phrase, ->, verb, noun_phrase);
composite_rule!(sentence, ->, noun_phrase, verb_phrase);
simple_rule!(article, ->, "a the");
simple_rule!(noun, ->, "man ball woman table");
simple_rule!(verb, ->, "hit took saw liked");
fn conv_str(v:Vec<&'static str>) -> String {
let mut cs:String = "".to_string();
for s in v {
cs = cs + s + " ";
}
cs + "."
}
fn main() {
for _ in 0..10 {
println!("{}", conv_str(sentence()));
}
}