静かなる名辞

pythonと読書

【common lisp】common lispでn-gram

 趣味で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でリストを作るだけなので難しいことはありません。