Twitterのタイムラインを表示する
今回は、Twitterのタイムラインから会話を抽出するNuのプログラムを作ってみます。プログラムは、nush上で関数を1つ定義しては動作させながら作っていく、というのが良いと思います。
Twitterからタイムラインを取得してこないとプログラムは始まりませんが、まずはLispらしいプログラムを説明するために、Twitterが取得したタイムラインが以下のようなS式になっているとしましょう。
((7736671823 hituji おはよう、寝不足・・・ 7736671819) (7736671822 rakuda おはようございます 0) (7736671821 uma おはよう、朝ご飯食べすぎた 7736671819) (7736671819 ushi おはよう、良い天気です 0))
このS式は発言が並んだもので、1つの発言は (ID 名前 発言 リプライ先のID) になっています。オブジェクト指向言語では通常、プログラムが扱うデータをクラスとして定義し、それらを操作する処理を定義してプログラムを作っていきますが、それほど複雑ではないデータの場合、Lispでは上のようにS式の並びでデータ表現してしまうことで、手軽にプログラミングが始められます。
前ページで説明したように、リストの最初の要素を取り出すのが car関数です。2番目の要素は (car (cdr x)) といった具合に cdr で最初の要素を除いたリストを取得し、その先頭を car で取り出しています。一見まわりくどいように思えますが、前ページのS式の図を見ると分かるように、car、cdr ともにポインターをたぐっているだけです。
3番目の要素は (car (cdr (cdr x))) で取得できますが、毎回このように書くのではコードが分かりにくくなるので、以下のような参照用関数を定義しておきましょう。
(function tl-id (e) (car e)) (function tl-name (e) (car (cdr e))) (function tl-text (e) (car (cdr (cdr e)))) (function tl-reply-id (e) (car (cdr (cdr (cdr e)))))
タイムラインを表示する関数
結果表示やデバック用にタイムラインを見やすく表示する関数を定義しましょう。1つの発言を表示する関数は以下のようになります。文字列の中の #{式} へは、Ruby同様に式の値を文字列に埋め込みます。
(function tl-puts (e)
(puts "#{(tl-id e)} #{(tl-name e)} #{(tl-text e)} #{(tl-reply-id e)}") t)
次にタイムライン全体の表示は以下のようになります。
(function tl-list-puts (tl)
(cond ((null tl) nil)
(else (tl-puts (car tl))
(tl-list-puts (cdr tl)))))
cond は if ... elsif ... elsif ... else ... end に相当するNu組込のオペレータです。まず、タイムライン(引数 tl)が空なら何もしません(単に nil を戻します)。それ以外の場合は tlの先頭要素 (car tl) を tl-puts関数で表示し、残り(cdr tl)を自分自身に渡しています。
Lispではこのように、ある条件の場合は処理し、それ以外の場合は自分自身を呼び出すという再帰的な呼び出しがよく使われます。もちろん、この処理は while等の繰り返しを使って書いてもかまいませんが、再帰呼び出しを使うと作業用の変数などを使わずに済みます。
リストが空かどうかを判断する null関数の定義は以下のようになります。
(function null (x) (== x ()))
タイムラインから会話を取り出す
タイムラインの中で、ある人の発言に返答(リプライ)している会話のみを抽出してみましょう。会話は「リプライ先のIDがある発言」と「その元の発言をリストにしたもの」のリストとします。最初に書いたタイムラインの例では
(((7736671823 hituji おはよう、寝不足・・・ 7736671819) (7736671819 ushi おはよう、良い天気です 0)) ((7736671821 uma おはよう、朝ご飯食べすぎた 7736671819) (7736671819 ushi おはよう、良い天気です 0)))
となります。
IDを指定して、その発言を取得する (find-by-id タイムライン ID) 関数があるとすると、会話の取得は以下のようになります。
(function find-talk (tl)
(cond ((null tl) ())
((!= (tl-reply-id (car tl)) 0)
(cons (list (car tl) (find-by-id tl (tl-reply-id (car tl))))
(find-talk (cdr tl))))
(else (find-talk (cdr tl)))))
- タイムラインが空なら空リストを戻す。
- タイムラインの先頭がリプライ先IDを持っていれば(0でなければ)その発言とリプライ先IDの発言を list関数で1つのリストにします(list関数は発言全体をコピーしたのでは無く、それぞれの発言へのポインターのリストができるだけです)。
- 上の値とタイムラインの2番目以降の会話をfind-talkを呼び出し cons で連結しています。
- タイムラインの先頭がリプライ先IDを持っていなければ、2番目以降の会話をfind-talkを呼び出し求めています。
この関数も、再帰的に2番目以降の会話を求めています。さらに、その結果と1番目の結果をconsでつなぐ事で結果のリストを組み立ています。このようにS式(リスト)と再帰的呼び出しは非常に相性がよいのです。ただし、再帰的呼び出しはスタックを消費するので、再帰が深くなる場合は注意が必要です。
ID指定で、その発言を取得する find-by-id関数は以下のようになります。ここでも再帰的呼び出しを使っています。
(function find-by-id (tl id)
(cond ((null tl) nil)
((== (tl-id (car tl)) id)
(car tl))
(else (find-by-id (cdr tl) id))))
抽出された会話を見やすく表示する関数も定義しておきましょう。この関数も再帰的呼び出しを使っています。また、すべてのタイムラインは取得できないのでリプライ先IDの発言がない場合があります。その判断も必要になります。
(function talk-list-puts (talk-list)
(cond ((null talk-list) nil)
((null (car talk-list)) nil)
(else (tl-list-puts (car talk-list))
(puts "-------")
(talk-list-puts (cdr talk-list)))))
