LoginSignup
1
0

More than 3 years have passed since last update.

RustのマクロでPAIPの2章 A Rule-Based Solutionをやってみた

Posted at

古き良き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()));
    }
}
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0