趣味でCommon Lispを始めました。とりあえず練習にn-gramを書いてみました。
書き方は色々あると思いますが、ループ構文がまだいまいちわからないので再帰で書きます。
(defun rec-ngram (list n &optional (ret-list '())) (if (eq (length list) (- n 1)) ret-list (rec-ngram (cdr list) n (nconc ret-list (list (subseq list 0 n))))))
いけてる書き方なのかどうかは良くわかりませんが、そう複雑なものではありません。末尾再帰で書いていますが、末尾再帰でなくて良いならもっと簡単に書けます。
使い方はこんな感じ。
* (rec-ngram '("吾輩" "は" "猫" "で" "ある" "。") 2) (("吾輩" "は") ("は" "猫") ("猫" "で") ("で" "ある") ("ある" "。"))
けど、毎回listの長さを計算するのは馬鹿馬鹿しいと思ったので、最初に全体の長さを計算し、後は1ずつ引いていくことにしました。
(defun rec-ngram (list n &optional (list-length nil) (ret-list '())) (if (eq list-length nil) (setq list-length (length list))) (if (eq (length list) (- n 1)) ret-list (rec-ngram (cdr list) n (- list-length 1) (nconc ret-list (list (subseq list 0 n))))))
こんな感じでn-gramが作れますが、以前書いたpython版と比べると機能的に見劣りします。
- 文字列を引数に取れない
- n-gramの要素がリストで返ってきても嬉しくない(区切り文字列で区切られた文字列の方が嬉しい)
とりあえずpython版と同じ機能になるように、適当に関数を増やしてラップしてみます。
(defun ngram (str &key (n 2) (splitter "-*-")) ;文字列が来たら一文字ずつの文字列のリストに変換 (if (typep str 'string) (setq str (mapcar #'string (concatenate 'list str)))) ;任意の区切り文字でn-gramの結果を連結 (let ((ngram-result (rec-ngram str n))) (mapcar (lambda (list) (format nil (format nil "~~{~~A~~^~A~~}" splitter) list)) ngram-result)))
nと区切り文字列はキーワード引数にしてみました。list-lengthをこっちの関数で計算する前提にすればrec-ngramのif文を外せますが、面倒くさいのでやっていません。formatを二段重ねにしてる辺りがとても気持ち悪いですが、formatの書式指定がよくわからないので勘弁してください。こんな感じで使えます。
* (ngram "吾輩は猫である。") ("吾-*-輩" "輩-*-は" "は-*-猫" "猫-*-で" "で-*-あ" "あ-*-る" "る-*-。") * (ngram '("吾輩" "は" "猫" "で" "ある" "。")) ("吾輩-*-は" "は-*-猫" "猫-*-で" "で-*-ある" "ある-*-。") * (ngram "吾輩は猫である。" :n 3) ("吾-*-輩-*-は" "輩-*-は-*-猫" "は-*-猫-*-で" "猫-*-で-*-あ" "で-*-あ-*-る" "あ-*-る-*-。") * (ngram "吾輩は猫である。" :n 3 :splitter "!??!") ("吾!??!輩!??!は" "輩!??!は!??!猫" "は!??!猫!??!で" "猫!??!で!??!あ" "で!??!あ!??!る" "あ!??!る!??!。")
lispは書く分には楽しいですが、pythonと比べるとコード量が多いというか、低水準な印象です。使いこなせてない便利な機能も色々あると思うので、慣れてくればもう少し気楽に書けるようになるとは思います。とりあえず、しばらくは趣味で触っていくことにします。
追記 2017/03/16
ループでも書きました。
(defun loop-ngram (list n) (let ((result-list '())) (dotimes (count (- (length list) n -1) result-list) (setq result-list (cons (subseq list count (+ count n)) result-list)))))
シンプルですが、逆向きになって出てきます。
* (loop-ngram '("吾輩" "は" "猫" "で" "ある" "。") 2)) (("ある" "。") ("で" "ある") ("猫" "で") ("は" "猫") ("吾輩" "は"))
逆向きがいやならreverseするか、最初からnconcでリストを作るだけなので難しいことはありません。