一般公開「りんご味Ruby:藤本尚邦(連載終了)」
(MOSADeN Onlineでは、掲載日から180日を経過した記事を一般公開しています。)- 第61回 雑記帳アプリを作る#10 –メモ書きテキストのパース(2010/01/28:掲載)
- 第60回 雑記帳アプリを作る#9 — メモ書きテキストにデータを埋め込む(2010/01/12:掲載)
- 第59回 雑記帳アプリを作る#8 — メタデータ検索(2009/12/22:掲載)
- 第58回 雑記帳アプリを作る#7 — Webアーカイブ形式の読み書き(2009/12/08:掲載)
- 第57回 雑記帳アプリを作る#6 — メモエディタ(2009/11/24:掲載)
- 第56回 雑記帳アプリを作る#5 — AppControllerの実装(2009/11/10:掲載)
- 第55回 雑記帳アプリを作る#4 — アプリケーションのひな形(2009/10/27:掲載)
- 第54回 雑記帳アプリを作る#3 — メモスタック(2009/10/13:掲載)
- 第53回 エンドユーザプログラミング環境を作る#1 — 雑記帳アプリ(2009/09/27:掲載)
- 第52回 エンドユーザプログラミング環境を作る#0(2009/09/13:掲載)
-
第61回 雑記帳アプリを作る#10 –メモ書きテキストのパース
この記事は、2010年01月28日に掲載されました。今回は、メモ書きテキストを構文解析して、埋込まれたデータを取り出すプログラムを作ります。
■ メモ書きテキストの構文
まず、メモ書きテキストの構文を確認しましょう。
埋込みデータの開始条件、つまり、メモ書きテキストの中にこういうパターンがあれば、そこからが埋込みデータですよ、という文字列のパターンは、次のいずれかとします。
- 「行頭」から「空白」以外の最初の文字が “(”
- “#(”
埋込みデータ全体は、上のどちらかのパターンで始まり、データ列(データの並び)が続き、データの終了を表す右括弧 “)” で終わります。データは:
- リスト(=入れ子になったデータ列)
- 数値 (整数または小数)
- その他の文字列
のいずれかということにします。データ列内の各データは、スペース、タブ、改行などの空白文字で区切られます。
以上がおおよその構文になりますが、まだ不完全な上に、日本語で表現するとごちゃごちゃしてわかりにくいですね。もう少し簡潔かつ形式的にするため、バッカス・ナウア記法(BNF)という記法を使って表現します。BNFは、プログラミング言語などの文法を、形式的に表現するためによく使われる記法です。
<埋込みデータ> ::= <埋込みデータ開始条件> <データ列> <右括弧> <リスト> ::= <左括弧> <データ列> <右括弧> <データ列> ::= <データ> | <空白> | <空白> <データ列> <データ> ::= <リスト> | <整数値> | <小数値> | <その他の文字列>
BNFで定義しなかった細かい構文要素は、Rubyの正規表現リテラルで定義します。
PATTERN_ROOT_LPARENT = /^(\s*|.*#)\(/ # <埋込みデータ開始条件> PATTERN_WS = /\s+/m # <空白> PATTERN_LPARENT = /\(/ # <左括弧> PATTERN_RPARENT = /\)/ # <右括弧> PATTERN_INTEGER = /[+-]?\d(\d|,\d\d\d)*/ # <整数値> PATTERN_DECIMAL = /[+-]?\d(\d|,\d\d\d)*\.\d+/ # <小数値> PATTERN_SYMBOL = /[^\s()]+/ # <その他の文字列>
数値に関しては、本気で定義しようとするとかなり大変そうなので、とりあえず、整数値と小数値(有理数)のみごく簡単に対応しました。1,234,567 のような “,” を使う記法も使えるようにしてみました。
以上で、メモ書きテキストの構文解析のための材料は、だいたい揃いました。
■ パーサの実装
実装には、strscanライブラリの文字列スキャナー StringScanner を使います。
@scanner = StringScanner.new(メモ書き文字列)
文字列スキャナーを使って、文字列を先頭からスキャンポイント(調べる位置)を移動しながら文字列を走査します。以下の2つのメソッドを使います。
- StringScanner#scan — 引数で与えられたパターン(正規表現)がスキャンポイントでマッチすれば、スキャンポイントを進めてマッチした文字列を返す
- StringScanner#skip — scan とほぼ同じ。マッチした文字列のかわりに、スキャンポイントを進めた分の長さを返す
では、パーサの本体(parseメソッド)を実装しましょう。埋込みデータを探して構文解析し、結果を配列に追加していきます。
def parse result = [] while not @scanner.eos? do if x = @scanner.scan(PATTERN_ROOT_LPARENT) then result << parse_list else @scanner.skip(PATTERN_SKIP) end end result end
埋込みデータ開始条件(PATTERN_ROOT_LPARENT)にマッチするまで文字列を読み飛ばします。マッチしたら、そこが埋め込みデータ全体を含むリストの開始になるので、リストを解析する parse_list を呼び出します。結果は、配列result に追加します。マッチしなかった場合は、1行分を読み飛ばします(PATTERN_SKIP)。
これを、スキャンポイントが文字列の最後に達するまで繰り返します。次に、リストのパース(parse_listメソッド)を実装します。
def parse_list result = [] while not @scanner.scan(PATTERN_RPARENT) do @scanner.skip(PATTERN_WS) case when x = @scanner.scan(PATTERN_LPARENT) then result << parse_list when x = @scanner.scan(PATTERN_DECIMAL) then result << x.gsub(/,/,'').to_f when x = @scanner.scan(PATTERN_INTEGER) then result << x.gsub(/,/,'').to_i when x = @scanner.scan(PATTERN_SYMBOL) then result << x.strip else break # raise "unknown pattern" end end result end
空白を読み飛ばしながら、リスト・小数値(有理数)・整数値・その他の文字列のいずれかのスキャンして、データを配列resultに追加するというコードを、右括弧(PATTERN_RPARENT)に達するまで繰り返します。数値データの場合は、”,”を取り除いてから Integer または Float に変換します。
これで、メモ書きテキスト埋込みデータのパースが完成しました。いかがでしょうか?とても簡潔に実装できたと思いませんか?今回作ったパーサのプログラム全文(簡単なテスト付き)を本文の最後に貼りますので、興味のある方は irbなどを使って動かしてみてください。
require 'strscan' # メモ書きテキストの埋込データの構文定義 # # <埋込みデータ> ::= <埋込みデータ開始条件> <データ列> <右括弧> # <リスト> ::= <左括弧> <データ列> <右括弧> # <データ列> ::= <データ> | <空白> | <空白> <データ列> # <データ> ::= <リスト> | <整数値> | <小数値> | <その他の文字列> class MemoTextParser def self.parse(str) new(str).parse end def initialize(str) @scanner = StringScanner.new(str) end PATTERN_ROOT_LPARENT = /^(\s*|.*#)\(/ # <埋込みデータ開始条件> PATTERN_SKIP = /.*$\n?/ PATTERN_WS = /\s+/m # <空白> PATTERN_LPARENT = /\(/ # <左括弧> PATTERN_RPARENT = /\)/ # <右括弧> PATTERN_INTEGER = /[+-]?\d(\d|,\d\d\d)*/ # <整数値> PATTERN_DECIMAL = /[+-]?\d(\d|,\d\d\d)*\.\d+/ # <小数値> PATTERN_SYMBOL = /[^\s()]+/ # <その他の文字列> def parse result = [] while not @scanner.eos? do if x = @scanner.scan(PATTERN_ROOT_LPARENT) then result << parse_list else @scanner.skip(PATTERN_SKIP) end end result end def parse_list result = [] while not @scanner.scan(PATTERN_RPARENT) do @scanner.skip(PATTERN_WS) case when x = @scanner.scan(PATTERN_LPARENT) then result << parse_list when x = @scanner.scan(PATTERN_DECIMAL) then result << x.gsub(/,/,'').to_f when x = @scanner.scan(PATTERN_INTEGER) then result << x.gsub(/,/,'').to_i when x = @scanner.scan(PATTERN_SYMBOL) then result << x.strip else break # raise "unknown pattern" end end result end end if __FILE__ == $0 then require 'test/unit' class TestMemoTextParser < Test::Unit::TestCase def test_list memo = <<-MEMO_LIST ignore (hello 123 world 456) inline #(hello 123 world 456) ignore MEMO_LIST expected = [['hello', 123, 'world', 456], ['hello', 123, 'world', 456]] assert_parse(expected, memo) end def test_alist memo = <<-MEMO_ALIST ignore ((hello 123) (world 456)) inline #((hello 123) (world 456)) ignore MEMO_ALIST expected = [[['hello', 123], ['world', 456]], [['hello', 123], ['world', 456]]] assert_parse(expected, memo) end def test_multi_line memo = "モサ伝の記事を書いた...下書きをアップロード\n\n" << "昼食をコンビニで購入\n" << "(支出 (おにぎり (110 120))\n" << "(お茶500ml 110))\n\n" << "新宿から渋谷まで移動\n" << "(支出 (JR山手線 150 SUICA))\n\n" << "きょうはおこづかいの日\n" << "(収入 (こづかい 25,000 口座振込))\n\n" << "本を購入#(支出 (本 1,800))、本屋で立ち読みしてたら面白かったので\n" expected = [ ['支出', ['おにぎり', [110, 120]], ['お茶500ml', 110]], # 1つめの埋込データ ['支出', ['JR山手線', 150, 'SUICA']], # 2つめ ['収入', ['こづかい', 25000, '口座振込']], # 3つめ ['支出', ['本', 1800]], # 4つめ ] assert_parse(expected, memo) end private def assert_parse(expected, memo, msg=nil) assert_equal(expected, MemoTextParser.parse(memo), msg) end end end
-
第60回 雑記帳アプリを作る#9 — メモ書きテキストにデータを埋め込む
この記事は、2010年01月12日に掲載されました。前回まで、しばらくの間、ユーザインタフェースまわりの実装に軸足を置いてきましたが、今回は、雑記帳のデータベースの設計・実装、とくにメモのテキスト形式の表記法・ルールについて考えます。
第53回ではメモの表記法について取り上げました。メモのテキスト形式の表記方法に簡単なルールを導入して、メモ書きテキストの中にユーザプログラムから扱い易いデータを埋め込もうというのが、その趣旨でした。この表記方法についてもう少し掘り下げてみましょう。以下は、あるルールでデータを埋め込んだ雑記帳アプリ用のメモ書きテキスト例です。
モサ伝の記事を書いた...下書きをアップロード 昼食をコンビニで購入 (支出 (おにぎり (110 120)) (お茶500ml 110)) 新宿から渋谷まで移動 (支出 (JR山手線 150 SUICA)) きょうはおこづかいの日 (収入 (こづかい 25,000 口座振込)) 本を購入#(支出 (本 1,800))、本屋で立ち読みしてたら面白かったので
この表記のルールのポイントは、括弧を使って木構造データを表現しているところにあります。
- 行頭の左括弧、あるいは”#”に続く左括弧から、それに対応する右括弧で囲まれた部分に、ユーザプログラムが扱うためのデータ構造を書く
- 括弧はデータの階層構造(木構造)の深さを表す
- データ構造内の個々のデータはスペースで区切る
というのがおおよそのルールになります。
このようにメモ書きテキストに埋め込まれたデータを、プログラム中でオプジェクトとして扱うときにどのように表現するのがよいでしょうか?もちろん、そのまま木構造を扱うクラスを定義して扱う(XMLのDOMなどのように)という方法もありますが、ここではひとまず、配列の入れ子でお手軽に表現してしまうことにします。
['支出', ['おにぎり', [110, 120]], ['お茶500ml', 110]] # 1つめの埋込データ ['支出', ['JR山手線', 150, 'SUICA']] # 2つめ ['収入', ['こづかい', 25000, '口座振込']] # 3つめ ['支出', ['本', 1800]] # 4つめ
木構造内の個々のデータは、基本的には、文字列オブジェクトにします(あるいはRubyのシンボルオブジェクトでも良いでしょう)。ただし、データが数値らしいときには数値オブジェクトに変換します。例えば、”25,000″ というデータをメモ書きテキストに埋め込んだとします。これは、おそらく、数値として扱いたいデータでしょうから、数値オブジェクトに変換することにします。
(余談: ある表記について、そのデータをどのような型のデータとして扱いたいかについては、誰にも満足がいくように境目を決めるのは難しいところではあるのですが)
このように埋め込まれたデータを、メモオブジェクトから(あるいは検索を通じて)取り出すことのできるようなプログラムインターフェースがあれば、さまざまな応用ができるようになるでしょう。
例えば、前述のようなメモを記録した雑記帳データベースについて、小遣いの収支を計算するようなプログラムが可能になります。まず、1つの支出または収入データについての金額を計算するメソッドを定義しておきます。
def calc_amount(i) # i は: # ['支出', [<項目>, <金額>, ...], # [<項目>, [<金額>, ...], ...], # ....] # のような形式のデータ構造 val = 0 i[1..-1].each do |j| case j[1] # <金額> または [<金額>, ...] when Numeric then val += j[1] when Array then val += j[1].inject { |r,k| r + k } else raise "unknown format" # 例外を上げるよりは無視する方がいいかもしれない end end val end
ある月のメモを検索して集めたメモの配列を memos とすると、以下のようなコードでその月の収支を計算することができます。
memos # ある月のすべてのメモの配列 (検索して集めた) expenses = incomes = 0 memos.each do |memo| memo.embedded_datas.select_all('支出').each { |x| expenses += calc_amount(x) } memo.embedded_datas.select_all('収入').each { |x| incomes += calc_amount(x) } end puts "収入: #{incomes}" puts "支出: #{expenses}" puts "収支: #{incomes - expenses}"
他にも、csv形式などで保存して表計算アプリに流し込むなど、いろいろな応用が考えられるでしょう。
さて、メモに記録する収支の具体的な表記ルールと、収支を計算するユーザプログラムとの間には、表裏一体の関係にあります。最初に提示したメモ書きテキスト例にあった収支データの表記を以下のように変更すると、より読みやすいかもしれません。
モサ伝の記事を書いた...下書きをアップロード 昼食をコンビニで購入 (買物 (おにぎり (110 120)) (お茶500ml 110)) 電車で移動 (交通費 (JR山手線 150 SUICA) (銀座線 160 SUICA)) きょうはおこづかいの日 (こづかい 25,000 口座振込) 本を購入#(買物 本 1,800)、本屋で立ち読みしてたら面白かったので
しかし、このような収支表記にすると、支出と収入の区別をプログラム側で判断する必要(テーブルを持つなど)が出てくるので、おそらく収支計算のプログラムの方はやや複雑になるでしょう。表記ルールの読みやすさとプログラムの複雑さのバランスをどのように取るべきか難しいところではありますが、一方で、プログラミングするユーザが自由に決めることもできます。
今回提示した、メモ書きテキストの埋込みデータの表記法は、Lispのプログラムやデータで用いられているS式と呼ばれる記法をベース(参考)にしています。データをメモ書きテキストに埋め込むのにもっとよい記法はないものかなと考えてみましたが、今のところはこの記法に落ち着きそうです。何か他にもよいアイディアがあれば教えてください。ということで、次回に続きます。
-
第59回 雑記帳アプリを作る#8 — メタデータ検索
この記事は、2009年12月22日に掲載されました。これまでに、雑記帳アプリのユーザインタフェースとして、新しいメモと最後に作成したメモを編集・保存するためのメモエディタを実装しました。今回は、メモスタックを検索する検索ウィンドウを実装します。検索にはMac OS XのSpotlightで使われているメタデータ検索機能を使うことにします。
今回は、これから説明する検索機能までを実装した雑記帳アプリケーションのサンプルプロジェクトを用意しました(MosadenMemoStack-059)ので合わせてご覧ください。
■ 検索の流れ
検索の流れをRubyプログラムとして書き下すと以下のようになります。
query = NSMetadataQuery.alloc.init query.searchScopes = [ "#{ENV['HOME']}/Documents/MosadenMemoStack" ] pred_kind = NSPredicate.predicateWithFormat("kMDItemKind like[c] %@", "Web*") text = "ネコ" # ユーザが入力したテキスト pred_text = NSPredicate.predicateWithFormat("kMDItemTextContent like[c] %@", "#{text}*") ary = [ pred_kind, pred_text ] query.predicate = NSCompoundPredicate.andPredicateWithSubpredicates(ary) query.startQuery
まず、検索オブジェクト(Cocoa の NSMetadataQueryクラスのインスタンス)を生成します。NSMetadataQuery は、Spotlightのメタデータ検索機能をカプセル化したObjective-Cのクラスです。
query = NSMetadataQuery.alloc.init
次に、検索する範囲(検索対象のフォルダ)を指定します。
query.searchScopes = [ "#{ENV['HOME']}/Documents/MosadenMemoStack" ] # (1)
次に、検索の条件を設定します。雑記帳メモはWebarchive形式で保存していますから、Webarchive形式で絞りこむことにします。
pred_kind = NSPredicate. predicateWithFormat("kMDItemKind like[c] %@", "Web*")
ユーザが入力したテキストでも絞り込みます。
text = "ネコ" # ユーザが入力したテキスト pred_text = NSPredicate. predicateWithFormat("kMDItemTextContent like[c] %@", "#{text}*")
以上2つの条件のAND(論理積)を検索条件として設定します。
ary = [ pred_kind, pred_text ] query.predicate = NSCompoundPredicate.andPredicateWithSubpredicates(ary)
条件を設定したら検索を開始します。
query.startQuery
startQuery は非同期的に検索を実行します。検索実行中の状態の変化(検索終了など)は、デリゲートあるいは通知(Notification) として受け取ることができます。サンプルでは、Nibファイル(MemoQueryWindow.xib)で、テーブルカラムなどにバインディングを設定して表示しています。
最後に、検索結果からメモオブジェクトを得るコードが必要です。これは、Cocoa/Objective-Cの作法に忠実に書くならば、NSMetadataQuery のデリゲートメソッド metadataQuery:replacementObjectForResultObject: に実装することになるでしょう:
def metadataQuery(q, replacementObjectForResultObject:item) path = item.valueForAttribute("kMDItemPath") mid = File.basename(File.dirname(path)) AppController.memoStack[mid] end
もちろんこれでいいのですが、サンプルでは、Cocoaバインディングを使って扱いやすくするために、また Ruby での手軽な方法として、検索結果アイテム(NSMetadataItem)にインスタンスメソッドを直接実装してしまいました。
class NSMetadataItem def memo if @memo.nil? then path = self.valueForAttribute("kMDItemPath") mid = File.basename(File.dirname(path)) @memo = AppController.memoStack[mid] end @memo end end
今の時点では NSMetadataItem をメモの検索結果以外に使っていないのでこの方法でも問題はないのですが、別の検索に使うかもしれない可能性ということを考えると、あまり行儀のよいコードとはいえないでしょう。あとでちゃんとした設計を考えるとして、とりあえず動くコードを手早く実装するというのがこの実装の意味合いになります。
■ (補足) setter メソッドの別名
これまで説明せずに使っていましたが、MacRubyでは、名前が “set” で始まるsetter メソッドの呼び出し:
query.setSearchScopes([ "#{ENV['HOME']}/Documents/MosadenMemoStack" ]) query.setPredicate(NSCompoundPredicate.andPredicateWithSubpredicates(ary))
を、以下のように書くことができます。
query.searchScopes = [ "#{ENV['HOME']}/Documents/MosadenMemoStack" ] query.predicate = NSCompoundPredicate.andPredicateWithSubpredicates(ary)
元の setter メソッドの名前が “setXxxYyyZzz” の場合、”xxxYyyZzz=” という別名があるわけですね。この書き方は代入構文のように見えるかもしれませんが、そうではなくて、オブジェクト query をレシーバとしたメソッドの呼び出しになります。
■ まとめ
本文では説明できませんでしたが、ここまでに述べてきた検索の流れに基づいた検索ウィンドウは、サンプルの MemoQueryController.rb とMemoQueryWindow.xib で実装していますので参考にしてください。
ここしばらく、ユーザインタフェースまわりの実装に軸足が傾いていました。非常にプリミティブな機能に限られていますが、最低限必要な機能のたたき台くらいは実装できたのではないでしょうか。今回、検索機能を実装してみて、メモスタックデータベースの設計・実装をあらためて考え直す必要を感じています。ということで次回に続きます。
-
第58回 雑記帳アプリを作る#7 — Webアーカイブ形式の読み書き
この記事は、2009年12月08日に掲載されました。前回はメモエディタとしての流れに軸足を置いていました。今回は軸足をメモとメモスタックデータベースに移して、メモエディタの実装の細かいところを考えていきます。
■ メモデータのフォーマット
メモのデータは、第54回でも触れたように、Webアーカイブ形式でメモスタックに保存します。NSTextView に読み書きできること、NSTextViewやWebViewを画像表示やリンクのユーザーインターフェースとして使えることなどが、Webアーカイブ形式のメリットです。
あわせて、単純なテキスト形式でも保存することにします。なんだか忘れそうになっていますが、雑記帳アプリケーションはユーザプログラミング可能な環境でもあって、テキストはユーザプログラムが扱うデータでもあるのです。また、自動キーワード抽出のような機能を持つような場合にも、テキスト形式があれば、その方がより扱い易いでしょう。
■ メモの保存 (save_memo)
以上のことを踏まえて、メモエディタでメモを保存するメソッドであるsave_memo を実装しましょう:
def save_memo @memo.text = bytes_as_text # テキスト形式データをセット @memo.webarchive = bytes_as_webarchive # Webアーカイブ形式データをセット @memo.update! # メモスタック更新 @window.title = @memo.title @dirty = false end
実は、上のsave_memoの定義は、前々回(第56回)に提示したもの:
def save_memo @memo.text = rawdata_of_text # テキスト形式データをセット @memo.data = rawdata_of_webarchive # Webアーカイブ形式データを # (メイン)データとしてセット @memo.kind = :webarchive # (メイン) @memo.update! ... 以下は同じ ...
と少し違っています。この記事を書いている最中に、前者のようにデータ形式に差をつけずに扱う方が良いのではないかと考えて、(いきあたりばったりではありますが)変更することにしました。
これまでは、メモが持つデータを2種類(メインのデータ形式であるWebアーカイブ形式と、補助的なデータとしてのテキスト形式の2つ) にわけて考えていました。これには何かしら違和感があったのですが、この記事を書いている最中に、
「メモの持つデータ形式が複数になっているけど、それならば、メインと補助のような区別をつけずに同列に扱う方がのちのち有効なのではないだろうか?」
という考えがふつふつと湧いてきたのでした。それぞれのデータ形式を同列に扱うのなら、メモデータのセットや取得はどのようなインターフェースになるだろうかと考えてみると、単純にデータ形式名そのものをsetter/getterのメソッド名にすれば良さそうな気がします:
# 設定 (setter) memo.text = 新しいテキストデータ memo.webarchive = 新しいWebアーカイブデータ # 取得 (getter) memo.text # => テキストデータ memo.webarchive # => Webアーカイブデータ
ということで、今回新しく save_memo の定義が登場したわけです。実は、この新しい定義にはまだ問題点(それが何だかわかるでしょうか?) があります。しかし、メモのデータ形式を同列に扱うという考え方そのものはスジが良さそうなので採用することにします。細かいことはあとで考えることにして、先に進みましょう。
メモにセットする各形式データのバイト列を表すメソッド (bytes_as_text,bytes_as_webarchive) の定義は以下のとおりです:
def bytes_of_text @textView.string end def bytes_of_webarchive @textView.bytes_as_webarchive end
テキストバイト列に関しては、MacRuby(0.4)では「バイト列=Ruby文字列」かつ「Ruby文字列=NSString」なので、単純に NSTextView#string メソッドを呼んでいます。
Webアーカイブバイト列の方は、NSTextView#bytes_as_webarchive メソッドを呼びます。といっても、NSTextView にはこのようなメソッドはありません。このメソッドは、objc_support.rb ファイルの中で定義します(後述)。
■ メモの読み込み (load_memo)
メモを読み込むメソッド load_memo の定義も、前述の変更にあわせ変更します:
def load_memo webarchive = @memo.webarchive if webarchive then @textView.bytes_as_webarchive = webarchive else @textView.string = @memo.text # テキスト形式は必ず保存する end @window.title = @memo.title @dirty = false end
■ Webアーカイブ変換コードを押し入れにしまう
部屋は一見するときれいなんだけど、実は何もかも押し入れの中に押し込んである。押し入れを開けたら物が雪崩のように崩れ落ちてくるかもしれない。こんな状況に身に覚えるのある方もいらっしゃるのではないでしょうか。objc_support.rb というファイルの役割は、そんな押し入れにちょっと似ています。
Webアーカイブデータのバイト列をNSTextView から取り出したりセットしたりするコードはどこに実装すべきでしょうか?とりあえず、試しに実装してみる段階では MemoEditor.rb の中が手軽でいいでしょう。実際、前々回提示したコードでは、NSTextViewのメソッドではなく、MemoEditorのメソッドとして実装していました。
しかし、メモエディタの設計という観点では、これのメソッドの役割が何かという点は重要だけど、それをどのようにやるか(どのような実装か?)という点はあまり重要ではありません。そこで、これらのコードはNSTextViewのメソッド bytes_as_webarchive, bytes_as_webarchive= に移動します。
NSTextViewというCocoaの既存のクラスに手を加えるという、やや黒魔術初級的な手段なので(AppleのObjective-Cのライブラリでもこの方法はわりと使われているような気もしますが)、押し入れに隠しておきたい。ということで、前述のobjc_support.rb の中に実装します(こういうときは崩れ落ちないように細心の注意を払った方がよいでしょう)。今回押し入れの中にしまったソースコードを示して次回に続きます。
class NSTextView def bytes_as_webarchive self.textStorage.bytes_as_webarchive end def set_bytes_as_webarchive(bytes) self.textStorage.bytes_as_webarchive = bytes end alias_method :bytes_as_webarchive=, :set_bytes_as_webarchive end class NSAttributedString def bytes_as_webarchive range = NSRange.new(0, self.length) dict = { NSDocumentTypeDocumentAttribute=>NSWebArchiveTextDocumentType } err = Pointer.new_with_type('@') data = self.dataFromRange(range, documentAttributes:dict, error:err) if data.nil? then msg = err[0].localizedDescription raise "rawdata_of_webarchive error -- #{msg}" end rawdata = ' ' * data.length # データ長の長さの文字列(バイト列)を準備 data.getBytes(rawdata, length:rawdata.size) rawdata end end class NSMutableAttributedString def bytes_as_webarchive=(bytes) if bytes then data = NSData.dataWithBytes(bytes, length:bytes.size) opts = {} dict = Pointer.new_with_type('@') # NSDictionary err = Pointer.new_with_type('@') # NSError astr = NSAttributedString.alloc. initWithData(data, options:opts, documentAttributes:dict, error:err) if astr.nil? then emsg = err[0].localizedDescription dmesg_fl __FILE__, __LINE__, emsg raise "read_webarchive error -- #{emsg}" end self.setAttributedString(astr) end self end end
-
第57回 雑記帳アプリを作る#6 — メモエディタ
この記事は、2009年11月24日に掲載されました。今回は、メモエディタのプログラム MemoEditor.rb を見ていきます。
■ メモエディタを開く
メモエディタを開くには、前回実装した AppController の newMemo やopenLatestMemo にあったように、開きたいメモを引数としてクラスメソッドMemoEditor.open を呼び出します。MemoEditor.open の実装を見てみましょう:
def MemoEditor.open(memo, opts={}) editor = find_editor_for(memo) if editor.nil? then editor = self.alloc.initWithMemo(memo, options:opts) editors << editor end editor.window.makeKeyAndOrderFront(self) editor end
見ての通りで何をするコードかおおよその察しはつくと思うのですが、ざっと読み下してみます。
まず、開きたいメモと関連づけられたメモエディタがすでに存在していないか探します。見つからなかった場合には、メモエディタを新しく生成して、メモエディタリスト(配列)に追加します。最後に、見つかった既存のメモエディタ、あるいは新規生成されたメモエディタのウインドウを最前面に持ってきてキー入力可能な状態にします。
既存のメモエディタを探すクラスメソッド find_editor_for は以下のように実装しています。
def self.find_editor_for memo editors.find {|e| e.memo.memo_id == memo.memo_id } end
1行のコードで済んでしまうのに、何故そのまま MemoEditor.open の中に書かずに、わざわざメソッドにするのでしょうか?それは、メソッド名を付けることによって何をしているかが明確になるからです。適切なメソッド名や変数名を付けることによって、コメントなしで読み下しやすい(意図がわかりやすい)コードになるものです。
加えて、この editors.find のブロックの中のメモが同じかどうか調べるコードの部分は、いずれより適切な実装に変更するつもりなので、find_editor_for という名前をつけて実装を抽象化しておくという意味もあります。
もうひとつ、MemoEditor.open の実装について書き加えます:
editors << editor
の箇所は、Ruby慣れしていない人にはややわかりにくいかもしれないので、以下のように書いた方がよかったかもしれません。
editors.push(editor)
push と << のどちらも、配列の末尾にオブジェクトを一つ追加する配列のインスタンスメソッドです。editors は、単なる変数のように見えてしまうかもしれませんが、生成したメモエディタを覚えておくための配列を返すMemoEditorのクラスメソッドです。実際の配列はクラス変数 @@editors として定義しています:
def self.editors if not defined? @@editors then @@editors = [] else @@editors end end
■ メモエディタの生成
メモエディタの生成は、MemoEditorWindow.xibファイルで定義したビューをロードして、ビューが出来上がったところでメモの内容をロードします。
def initWithMemo(memo, options:opts) @memo = memo @editable = if opts.has_key? :editable then opts[:editable] else true end NSBundle.loadNibNamed("MemoEditorWindow", owner:self) self end def awakeFromNib @textView.setEditable(@editable) load_memo end
ここで注目したいのは以下の箇所です。
def initWithMemo(memo, options:opts) ... end
これは、RubyCocoa だと
def initWithMemo_options(memo, opts)
と書くところですが、MacRuby は Objecitve-C の場合の:
-(id) initWithMemo:(MemoEditor*)memo options:(NSDictionary*)opts { ... }
に近い形で書くことができるように、Ruby の構文が拡張されています。これはメソッドの呼び出しに関しても同様です(前述の MemoEditor.open の実装の中で initWithMemo:options: を呼び出しているところ)。
■ メモエディタを閉じる
メモエディタは、ユーザがエディタウィンドウを閉じたときに閉じられます。これは、ウィンドウが閉じられてくるときに送信されるNSWindowのdelegateメソッド windowWillClose として実装することになります:
# as NSWindow delegate def windowWillClose(ntf) save_memo if need_update? self.class.editors.delete(self) end
これまた見ての通りですが、必要ならばメモを保存(データベースを更新)して、前述のメモエディタリストから自分自身を削除しています。
雑記帳アプリケーションでは、編集したメモの保存(データベースの更新)を、ユーザが意識することなくアプリが自動的にやることにしました。といっても、まだ自動化のロジックをどのようにするか決まっていないわけなのですが、少なくともエディタを終了するときには保存する必要があるでしょう。ということで、ここで save_memo により保存しているわけです。
■ Array == NSMutableArray
メモエディタの初期化メソッドの定義や呼び出し方に MacRuby の特徴が垣間見えましたが、他にも見えないところで 今回説明したコードの中に MacRuby の特徴が効いているところがあります。既存のメモエディタを覚えておく配列のeditors (が返すクラス変数 @@editors の指すオブジェクト)ですが、これはRubyの配列であると同時に、Fundationフレームワークの NSMutableArray でもあるのです。
Objective-Cランタイムの上に直接実装されている MacRuby では、Ruby のArray(配列)=NSMutableArray なのですね。同様に、初期化メソッド(initWithMemo:options:)の2つめの引数 opts は、Ruby の Hash であると同時に NSMutableDictionary でもあります。今回は出てきませんでしたが、Ruby文字列はNSMutableStringになっています。
(注: それぞれ Mutable なのは、Rubyの配列や文字列には、もともと Mutableなものしかないからです)
MacRuby版のirbコマンドである macirb で見てみましょう…
$ macirb --simple-prompt >> ary = [1,2,3,4,5] => [1, 2, 3, 4, 5] >> ary.class => NSMutableArray >> ary.class == NSMutableArray => true >> ary < [1, 2, 3, 4, 5, 6] >> ary.push(7) => [1, 2, 3, 4, 5, 6, 7] >> ary.map { |i| i ** 2 } => [1, 4, 9, 16, 25, 36, 49]
NSMutableArrayですが、普通のRubyの配列として、リテラル表記でオブジェクトを作ることもできるし、数値を入れることもできるわけです。このようにObjective-Cのオブジェクトをブリッジを介さず直接扱えるのがMacRubyということになります。
次回は、今回説明しきれなかった load_memo と save_memo の実装をみていくつもりです。
-
第56回 雑記帳アプリを作る#5 — AppControllerの実装
この記事は、2009年11月10日に掲載されました。今回は「新規メモ」と「最後に作ったメモ」コマンドを実装します。この2つのコマンドは、それぞれ、雑記帳アプリケーションのデータベースであるメモスタックから生成・検索したメモをメモエディタで開きます。このメモエディタで(最低限の)メモの読み書きができるところまで実装したいところです。
■ 唯一のメモスタックオブジェクト
雑記帳アプリケーションでは、データベースとして、アプリケーション内で唯一つのメモスタック(第54回で実装)を扱います。この唯一のメモスタックオブジェクトへは、AppController のクラスメソッド AppController.memoStack を介してアクセスすることにします。以下は、クラスメソッドAppController.memoStack の定義です。
def AppController.memoStack if not defined? @@memo_stack then path = AppConfig[:memo_stack_local_path] @@memo_stack = MemoStack(:local, :path=>path) end @@memo_stack end
このRubyプログラムはとても読み下しやすく理解しやすいと思うのですがいかがでしょうか?日本語に読み下すと:
- もし、クラス変数 @@memo_stack が未定義なら
- アプリケーション設定からメモスタックのローカルパスを取得して
- メモスタックオブジェクトを生成し @@memo_stack にセットする。
- @@memo_stack を返して終了
といったところです。慣れてくれば(メソッド名などをうまく仕込むと)、このような読み下しやすいコードが書けるのもRubyのいいところです。
雑記帳アプリケーションでは、このメモスタックオブジェクトに対してメッセージを送る(メソッドを呼び出す)ことにより、新しいメモを作ったり、既存のメモを検索することになります。
■ 「新規メモ」と「最後に作ったメモ」コマンドの実装
「新規メモ」コマンドは、新しいメモを生成してユーザが編集可能な状態にするコマンドです。メモスタックのcreate_memoメソッドを呼び出して新しいメモを生成し、メモを編集するためのメモエディタで開きます。
def newMemo(sender) memo = memoStack.create_memo # 新しいメモを作って MemoEditor.open(memo) # メモエディタで開く end
「最後に作ったメモ」コマンドは、メモスタックのlatest_memoメソッド(注1)で最後に作ったメモを検索してメモエディタで開きます。オプショナルなキーワード引数 :editalbe で編集の可能/不可能を指定しています。
(注1: 第54回のMemoStackの実装では、最後に作ったメモを検索するメソッドにlast_created という名前を付けましたがlatest_memo という名前に変更しました)
def openLatestMemo(sender) memo = memoStack.latest_memo # 最後に作ったメモを検索して MemoEditor.open(memo, :editable=>true) # メモエディタで開く end
アプリケーションの動作として、既存のメモをエディタで開いたときに(デフォルトでは)メモを編集不可としたい場合は、:editable=>false とすることができます。また、メモスタックのローカルパスの場合(AppController.memoStackの実装)と同様に、アプリケーション設定(AppConfig)からデフォルト値を決めるように変更することもできます。
ところで、newMemo、openLatestMemo のようなインスタンスメソッドからメモスタックへのアクセスするために、クラスメソッド AppController.memoStackを呼ぶときには:
memo = AppController.memoStack.create_memo # あるいは memo = self.class.memoStack.create_memo # self.class => AppController
のようにアクセスすることになります。ですが、前述の newMemo の実装では(openLatestMemoでも同様):
memo = memoStack.create_memo
のように書きました。これは、クラスメソッドを呼び出すための略記法があるわけではなく(Rubyの構文上、省略は不可能です)、あらかじめインスタンスメソッド AppContrller#memoStack を定義してそれを呼び出しています。
AppContrller#memoStack の定義は以下のとおりで、単純にクラスメソッドの方の memoStack を呼んでいるだけです:
def memoStack AppController.memoStack # あるいは self.class.memoStack でもよい end
なぜ直接クラスメソッドを呼ばずにインスタンスメソッドの方を呼ぶかというと、newMemo や openLatest などの AppController のインスタンスメソッドの記述を簡潔にして読みやすくするためです。
■ アプリケーション設定 (AppConfig)
前述のクラスメソッド AppController.memoStack では、メモスタックのローカルパスを AppConfig[:memo_stack_local_path] で取得していました。
アプリケーション設定(AppConfig)は、NSUserDefaultsをベースに、Macアプリケーションの環境設定(英語では Preferences)を扱うクラスです。以下の2つのクラスメソッドを使って、設定値を読み書きすることができます。
AppConfig[キー] # キーで指定された設定の値を返す AppConfig[キー] = 値 # キーで指定された設定に値をセット・保存
現時点では、AppConfigクラスは objc_support.rb というファイルの中で定義しています。
また、キーごとの初期値(一度もアプリケーション設定で保存されていない段階での値)は、app_defaults.rb というファイルに記述します。現時点では以下のような設定になっています:
AppConfig.defaults = { :memo_stack_local_path => "#{ENV['HOME']}/Documents/MosadenMemoStack", }
この設定により、雑記帳アプリケーションを起動すると、メモスタックのローカルデータベースが書類フォルダ内に「MosadenMemoStack」という名前で作成されます。この場所を変更したい場合は、:memo_stack_local_path キーに対応する値(パス名)を変更してください。
■ メモエディタの概略
メモエディタは、MemoEditor.rb(Rubyプログラムによるコントローラ)とMemoEditorWindow.xib(Interface Builderによるビューの定義ファイル)と2つのファイルで構成します。
MemoEditor.rb の概略は以下のとおりです。
class MemoEditor attr_writer :textView, :window def self.open(memo, opts={}) # memoに対応するエディタが開かれていなければ、エディタを生成 # メモエディタウィンドウを前面にする end end
MemoEditorWindow.xib は、Xcodeのファイルテンプレートの Cocoa/WindowXIB をベースに作成します。設定のポイントは以下のとおりです:
- File’s Owner のクラスを MemoEditor に設定
- Window の中に Text View を配置
- File’s Owner のアウトレット window を Window に接続
- File’s Owner のアウトレット textView を Text View に接続
- Window のアウトレット delegate を File’s Owner に接続
- Text View のアウトレット delegate を File’s Owner に接続
■ まとめ
今回は、AppControllerの実装から、メモエディタでメモの読み書きができるところまでを説明したかったのですが、メモエディタについてはさわりだけで終わってしまいました。メモエディタについては次回に続きます。
本文だけではなかなかピンとこないと思うので、メモエディタで最低限のメモの読み書きができるところ(本文で説明した「新規メモ」と「最後に作ったメモ」コマンドのみ)までを実装したXcodeプロジェクトを添付しました ( MemoStack_2009-11-08.zip )。プロジェクトのビルドにはMacRuby 0.4のインストールが必要です。興味のある方はご覧・お試しください。
■ [補足] 今回のソースコード
以下は、今回実装した AppController.rb のソースコード(全文)です:
class AppController def AppController.memoStack if not defined? @@memo_stack then path = AppConfig[:memo_stack_local_path] @@memo_stack = MemoStack(:local, :path=>path) end @@memo_stack end def memoStack AppController.memoStack end # コマンド: 新規メモ def newMemo(sender) memo = memoStack.create_memo # 新しいメモを作って MemoEditor.open(memo) # メモエディタで開く end # コマンド: 最後に作ったメモ def openLatestMemo(sender) if memo = memoStack.latest_memo then # 最後に作ったメモを MemoEditor.open(memo, :editable=>true) # メモエディタで開く end end end
以下は、AppConfig クラスのソースコード(objc_support.rbより抜粋)です:
framework 'Cocoa' class AppConfig @@instance = nil def self.instance() @@instance ||= new end def self.defaults() instance.defaults end def self.defaults=(dict) instance.defaults = dict end def self.[](key) instance[key] end def self.[]=(key, val) instance[key] = val end attr_accessor :defaults def initialize @defaults = {} end def [](key) val = NSUserDefaults.standardUserDefaults.objectForKey(key.to_s) val || @defaults[key.to_sym] end def []=(key, val) NSUserDefaults.standardUserDefaults.setObject(val, forKey: key.to_s) end end
以下は、MemoEditor.rb のソースコード(全文)です:
class MemoEditor attr_writer :textView attr_accessor :window attr_reader :memo def self.editors if not defined? @@editors then @@editors = [] else @@editors end end def self.find_editor_for memo editors.find{|e| e.memo.memo_id == memo.memo_id } end def self.open(memo, opts={}) editor = find_editor_for(memo) if editor.nil? then editor = self.alloc.initWithMemo(memo, options:opts) editors << editor end editor.window.makeKeyAndOrderFront(self) end def initWithMemo(memo, options:opts) @memo = memo @editable = if opts.has_key? :editable then opts[:editable] else true end NSBundle.loadNibNamed("MemoEditorWindow", owner:self) self end def awakeFromNib @textView.setEditable(@editable) load_memo end def toggleEditable(sender) @editable = ! @editable @textView.setEditable(@editable) end # as NSText delegate def textDidChange(ntf) @dirty = true end # as NSWindow delegate def windowWillClose(ntf) save_memo if need_update? self.class.editors.delete(self) end private def need_update? ! empty? and @dirty end def empty? (@textView.string =~ /\A\s*\z/) ? true : false end def load_memo case @memo.kind when :webarchive then load_webarchive when :txt then load_text else load_text end @window.title = @memo.title @dirty = false end def save_memo @memo.text = rawdata_of_text @memo.data = rawdata_of_webarchive @memo.kind = :webarchive @memo.update! @window.title = @memo.title @dirty = false end #### def load_text if text = @memo.text then @textView.string = text end end def load_webarchive if rawdata = @memo.data then data = NSData.dataWithBytes(rawdata, length:rawdata.size) opts = {} dict = Pointer.new_with_type('@') # NSDictionary err = Pointer.new_with_type('@') # NSError astr = NSAttributedString.alloc. initWithData(data, options:opts, documentAttributes:dict, error:err) if astr.nil? then emsg = err[0].localizedDescription dmesg_fl __FILE__, __LINE__, emsg raise "read_webarchive error -- #{emsg}" end @textView.textStorage.setAttributedString(astr) end end def rawdata_of_webarchive astr = @textView.textStorage range = NSRange.new(0, astr.length) dict = { NSDocumentTypeDocumentAttribute=>NSWebArchiveTextDocumentType } err = Pointer.new_with_type('@') data = astr.dataFromRange(range, documentAttributes:dict, error:err) if data.nil? then msg = err[0].localizedDescription raise "rawdata_of_webarchive error -- #{msg}" end rawdata = ' ' * data.length # データ長の長さの文字列(バイト列)を準備 data.getBytes(rawdata, length:rawdata.size) rawdata end def rawdata_of_text @textView.string end end
-
第55回 雑記帳アプリを作る#4 — アプリケーションのひな形
この記事は、2009年10月27日に掲載されました。前回は、雑記帳の基本的データであるメモとメモスタック(memo_stack.rb)をざっくりと実装しました。これをベースにRubyを使って雑記帳のCocoaアプリケーションを作り始めます。
■ RubyCocoa と MacRuby
Rubyを使ってCocoaアプリケーションを作るための選択肢として、RubyCocoa とMacRuby があります。RubyCocoa原作者としては RubyCocoa を選びたいところなのですが、ここは MacRuby 0.4 を使って作っていくことにします。
ただし、どうしても使いたいライブラリが使えないなど、状況によっては、途中で RubyCocoa (あるいはMacRubyの次バージョン)に変更する可能性はあります。今回の雑記帳アプリを MacRuby 0.4 と RubyCocoa 1.0.1 で実装してみましたが、実際のところ、オブジェクトの生成などに少し気を使えば、ソースコードの大部分は共有できそうだという感触を得ました。MacRuby では、メソッド呼び出しが Objective-C に近くなるように構文が拡張されています(以下のコード片は、MacRuby と RubyCocoa で構文の違いが顕著な例)。
# RubyCocoa 1.0.1 の場合 data = NSData.objc_send(:dataWithBytes,rawdata, :length,rawdata.size) opts = {} dict = OCObject.new # NSDictionary err = OCObject.new # NSError astr = NSAttributedString.alloc. objc_send(:initWithData,data, :options,opts, :documentAttributes,dict, :error,err)
# MacRuby 0.4 の場合 data = NSData.dataWithBytes(rawdata, length:rawdata.size) opts = {} dict = Pointer.new_with_type('@') # NSDictionary err = Pointer.new_with_type('@') # NSError astr = NSAttributedString.alloc. initWithData(data, options:opts, documentAttributes:dict, error:err)
あえて大きく違うところを例示したのですが、実際のプログラムでは、このような違いよりも Ruby プログラムとしての共通性の方がずっと大きいようです。
MacRuby 0.4 は、MacRubyのサイトからバイナリ(zip形式)をダウンロードして簡単にインストールできます。
(注: 本稿では、Rubyでのアプリケーションプログラミングにポイントを絞るため、MacRubyやRubyCocoaのインストール、XcodeやInterface Builderの使い方についての細かい説明は省略します。これらの詳細については、「RubyによるMacOSXデスクトップアプリケーション開発入門」などの書籍等を参考にしてください)
■ 新規プロジェクトの作成
MacRuby 0.4 のインストールが完了したら、新規プロジェクトを作成しましょう。Xcode の新規プロジェクトで、テンプレートに MacRuby Application を選んで MemoStack という名前(またはお好みの名前)でプロジェクトを生成してください。
■ AppController.rb
新規ファイルで、テンプレートに Ruby class を選んで AppController.rb という名前でファイルを生成して、以下のようなコードを書きます。
AppController はアプリケーションのデリゲートになります。新しいメモを作成する newMemo コマンドと最後に作ったメモを開く openLatestMemo コマンドを定義します。それぞれ、前回 memo_stack.rb のプログラムで定義した2つのメソッド MemoStack#create_memo と MemoStack#last_created に対応しています。2つのコマンドの中身の
not_impl __FILE__, __LINE__
は、まだ実装してないよという意味です。この行が実行されると、この箇所のファイル名と行番号と「まだ実装してないよ」というメッセージがコンソールに出力されます。 __FILE__ と __LINE__ は、Rubyプログラムの(実行時ではなく)構文解析時に、それぞれファイル名と行番号に展開されます。
applicationDidFinishLaunching で使っている dmesg は、いわゆるprintfデバック、つまりプログラムの要所にデバッグ用のメッセージを埋め込むときに使います。
dmesg(__FILE__, __LINE__, "変数hogeの値=%@", hoge)
3つのめの引数は、フォーマット文字列で、4つめ以降はフォーマット文字列中で展開されます。
■ rb_main.rb の編集
AppController.rb の中で使っていた not_impl と dmesg は、MacRubyアプリの中で最初に実行される Rubyプログラムである rb_main.rb に実装します。rb_main.rb の中で、framework ‘Cocoa’ の行のあとに以下のコードを追加してください。
■ MainMenu.nib の編集
最後に MainMenu.nib を編集します。ポイントは以下のとおりです。
(1) AppControllerインスタンスを生成:
(2) ApplicationのdelegateをAppControllerインスタンスに接続:
(3) Fileメニューに、メニューアイテム「New Memo」と「Open Latest Memo」を作成します。使わないメニューは適当に削除します。
(4)「New Memo」を first responder の newMemo に接続、「Open LatestMemo」を first responder の openLatestMemo に接続します。
■ ビルドして実行
これで雑記帳アプリのひな形ができました。ビルドして実行してみましょう。起動すると applicationDidFinishLaunching の中に書いた dmesg のメッセージがデバッグコンソールに出力されます。また、Fileメニューの「New Memo」や「Open Latest Memo」を実行すると「まだ実装されていないよ」というメッセージが出力されます。
ひな形はできたけど中身がない。というわけで次回は、まだ実装されていない「新規メモの作成」と「最後に作成したメモの表示」を扱うためのMemoEditor を実装します。
-
第54回 雑記帳アプリを作る#3 — メモスタック
この記事は、2009年10月13日に掲載されました。雑記帳アプリの基本的なデータには何があるか考えます。シンプルに考えると、メモ自体とメモを溜め込むデータベースの2つといったところになるでしょう。メモそのものを「メモ(Memo)」、データベースを「メモスタック(MemoStack)」と呼ぶことにします。
このスタックという呼び名ですが、データベース内部のデータ構造をLIFO(=Last In First Out) なスタック構造(最後に入れたものを最初に取り出すデータ構造)にするというわけではありません。メモを書いてどんどん積んでいくという雑記帳アプリのもっとも基本的な使い方からの連想で付けたものです。
(…というのは表向きの理由で、本当はいにしえのユーザプログラミング環境HyperCardのデータベースがスタックと呼ばれていたことからの連想だったりするかもしれません)
■■ メモスタックの生成とアクセス ■■
Rubyプログラムからメモスタックにアクセスするときには、以下のようにMemoStack.memo_stack メソッドを使うことにします。
require 'memo_stack' stack = MemoStack.memo_stack(:local, :path=>"/tmp/hogehoge.memostack")
このパターンは、Cocoaでよくある [NSApplication sharedApplication] のようなパターン、つまりそのクラスのインスタンスを返すクラスメソッドの呼び出しと同じような形です。ただし、2度続けて MemoStack と書くのは冗長(irbなどで試すときなど)なので、ショートカットとして以下のようにも書けるようにすることにします:
stack = MemoStack(:local, :path=>"/tmp/hogehoge.memostack")
1つめの引数 :local はデータベースをローカルに置くというような意味です。2つめ(以降)の引数はHashキーワード引数で、:pathはデータベースへのパス名を表します。指定されたパスにメモスタックが存在していない場合は、新しいメモスタックをローカルディスク上に作成します。
■■ メモの作成と保存 ■■
メモを作成して内容を更新し保存する流れは以下のようにすることにします:
memo = stack.create_memo # 新規メモを生成 memo.update!(:data=>"hello,world", :kind=>:txt) # メモの内容を変更してスタックに保存
メモの更新・保存のタイミングや、メモのデータ形式をどうするかについてはまだ決まっていません。暫定的に、生データとファイル名の接尾辞(suffix)を種類として指定する形にしました。次回以降、ユーザインターフェースの入力部分を作りながら試行錯誤して固めていくつもりです。今のところ、データ形式としてはWebKit の WebArchive を採用するつもりです。理由はいろいろあります:
- テキスト入力、URLや画像の貼付けに NSTextView を使って楽をしたい
- NSTextViewは、画像やリンク付きデータを WebArchive で読み書きできる
- そのままでもSpotlightで検索してくれるはず
- 当面、Mac で動くことだけを考える
- WebArchiveはWebKitの一部なので他のシステムでも使えるはず?
- 中身は結局HTMLなのでWebアプリ化などに応用しやすいはず
■■ 既存メモの取得 ■■
メモを取得するメソッドは、いろんな検索/問合せパターンで取得できるように、追々、充実させていきたいところですが、とりあえず最低限のものとして以下のようなものから作っていきます。
memo = stack.last_created # 最後に作成されたメモ data = memo.data # メモのデータ本体 kind = memo.kind # メモの種類(ファイル名のsuffix)
■■ memo_stack.rbのソースコード ■■
以上の方針でメモスタックとメモを実装したプログラム memo_stack.rb をざっくりと実装しました。今のところパフォーマンスなどいろいろ問題のありそうなコードになっていますが、目的は果たすようになっているはずです。直接本文に書くには大きすぎるのでリンクをたどって見てください。
このソースコードは、以下のように、メモスタックを定義したライブラリプログラムの部分と、そのライブラリプログラムをテストする部分をひとつのファイルにまとめた構成になっています。if __FILE__ == $0 then … end の部分がテストプログラムです。
# -*- coding:utf-8 -*- # memo_stack.rb # メモスタックやメモの実装 # ... # ... if __FILE__ == $0 then require 'test/unit' class TestMemoStack < Test::Unit::TestCase # テストプログラム # ... # ... end end
ライブラリとして使う場合は、Rubyプログラムの中で:
require 'memo_stack'
のように使います。テストを実行する場合は以下のように、rubyコマンド(あるいはmacrubyコマンド)に、パス名を引数として渡します:
$ ruby memo_stack.rb
■■ 今回のまとめ ■■
今回は、メモスタックの基本データ構造の概要を示した上で、それをざっくりと実装しました。データの入力だけはなるべく早めに動かしたいところなので、次回はGUI部分にとりかかる予定です。
(サブタイトルを「エンドユーザプログラミング環境を作る」から「雑記帳アプリを作る」に変更しました。引き続きユーザプログラミング環境を持つ雑記帳アプリを作っていきます)
-
第53回 エンドユーザプログラミング環境を作る#1 — 雑記帳アプリ
この記事は、2009年09月27日に掲載されました。前回は、エンドユーザプログラミング環境アプリとして何を作るか迷ったまま終わってしまいました。その後、あれこれ考えた結果、日々の備忘録・雑記帳・メモ書き・スクラップブック・ブックマークなど個人的なメモ帳・記録の用途に使うことのできるアプリケーション(とりあえず雑記帳アプリと呼ぶことにします)を作ってみることに決めました。
■■ なぜ雑記帳アプリなのか? ■■
私は、日々のちょっとしたメモ書き(作業記録・ニュース記事のURL・考え事・感想・どこへ出かけた・いくら使ったなどの雑多なこと)を、Emacsを使って1つのテキストファイルに記録しています。このテキストファイルには、日々の雑多なメモ書きを上に積み重ねていくように書き足しています。以下は架空の例です。
[2009-09-24] モサ伝にアップした記事を修正 (小遣い支出 2009-09-24 (整骨院 650)) [2009-09-23] モサ伝の記事を書かなくちゃ、雑記帳アプリについて書くつもり... 書いた...下書きをアップロード 昼食はコンビニで買った: (小遣い支出 2009-09-23 (おにぎり2つ (+ 110 120)) (お茶500ml 110))
一見したところでは雑然とした単なるメモ書きのようですが、この中で小遣い収支をメモしている箇所に注目してください。
(小遣い支出 2009-09-24 (整骨院 650)) (小遣い支出 2009-09-23 (おにぎり2つ (+ 110 120)) (お茶500ml 110))
実は、この小遣い収支記録の箇所は、S式と呼ばれている表記法(XMLと同じく、木構造のデータを表現する記法の一種)で書いています。メモ書きの入力に使っているEmacsのプログラミング環境(Emacs Lispプログラム)は、S式をデータとして解釈するのが得意です。つまり、小遣い収支の箇所について、Emacsでデータ処理しやすくなるように意識して書いているわけです。
このように、(小遣い支出のような)日々の定型的な記録の表記法に少しだけ気を使って(=コンピュータに擦り寄るとでも言いましょうか)記録することにより、一見したところ単なる雑多なメモ書きテキストに見えるけど、実はいろいろと計算(例えば月別の小遣いの収支計算とか)することのできる小遣い帳データベースになる可能性を秘めたテキストになるわけです。
ところが、正直なところ、今の時点ではこの表記方法によるメモ書きを完全に活かしきることができていません(長くなるので理由は省略します)。そこで、この考え方に基づいた雑記帳アプリを作ってみようと思い立ったわけです。
スクラップブック・メモ帳的に使えるアプリとしては、例えば Evernote などの発想と被るところもあったりするかもしれないのですが、自分でプログラミングする方なら、自分なりのちょっと違ったアイディアなどをお持ちの方も多いのではないでしょうか。
上に書いた表記法などのアイディアが、実際にどのくらいうまくいくのか・便利になるのか、とりあえずは、私自身が(Emacsでのテキスト雑記帳から乗り換えて)日々便利に使えるくらいのところを目指して、試行錯誤しながら実装をすすめてみることにします。結果的に、多くの方にとっても便利なものになれば幸いです。
■■ 雑記帳アプリの基本方針 ■■
雑記帳アプリを作っていく過程では、以下のポイントを基本方針として重視するつもりです。
- すぐ書ける
- すぐ貼れる
- すぐ探せる
- 煩わしくない
「すぐ書ける・すぐ貼れる」は、新しい項目を書きたい・貼りたいとき、入力可能な状態に至るまでの手間をできるだけシンプル・短くしようということです。ちょっとしたメモを書いたり、今見ているブラウザで見ているURLや画像などを貼ったり、といったことが雑記帳アプリを使う上でのもっとも基本的な作業になります。できるだけ手早くこの作業に取りかかれるようなユーザインターフェースにしたいところです。
「煩わしくない」は、できるだけユーザに煩わしいこと・余計なことをさせず自動化するということです。ちょっとしたメモを書くだけなのに、いちいちタイトルやファイル名などを考えたくはありません。また、データをファイルに保存したかどうか、ネット上のデータとの同期はどうなってるといったことを気にするのも煩わしいものです。この種の面倒で煩わしいことは、できるだけ自動化して、ユーザが気にかけなくてもいいようにしたいところです。
■■ 特徴・機能 ■■
今の時点で考えている機能や特徴を列挙してみます(これらすべてを実装するのは少し欲張りすぎかもしれません、妥協や変更をすることもあるでしょう)。
- テキストエディタのような入力インターフェース
- ユーザプログラミング環境
- 構造化されたデータをとけ込ませたテキスト表記
- データは自動的に保存あるいは同期
- 作成・更新日時を自動的に記録
- データをネット上に置くことができる
- Webアプリによるインターフェース
- iPhoneアプリによるインターフェース
基本的なデータの入力方法としては、「なぜ雑記帳アプリなのか?」で書いたことから連想されるように、テキストエディタ的な入力インターフェースを考えています。日々のメモ書きを単なるテキストファイル感覚で淡々と書き続けていたらデータベースになっていた、みたいな感じが理想です。一方で、写真・動画・音声などのスクラップブックとしても使えるといいなとも考えているのですが、そうなると、テキストエディタ的な入力インターフェースとどう折り合いをつけるのかちょっとした考えどころかもしれません。
■■ まとめ ■■
2回続けてまったくプログラムのかけらもない内容になってしまいました。今回、やや風呂敷を広げ過ぎたという気がしつつも、どんなものを作るのかおおよそのところの方針・方向が決まりました。
プログラムが創られていく過程には、アイディアや仕様などを十分に練った上で淡々と実装していくという道と、実装の試行錯誤を繰り返しながら創っていくという道の二種類があるような気がします。私の場合、(困ったことに) 後者が性に合っているみたいです。同時に、プログラミングというのは後者のような方法が向いているジャンルであるとも考えています。ということで次回から、後者の道を進んで実装にとりかかることにします。
-
第52回 エンドユーザプログラミング環境を作る#0
この記事は、2009年09月13日に掲載されました。モサ伝がWebに移行するという話を聞いて、高橋編集長に「Rubyベースでエンドユーザプログラミング環境アプリみたいなものを試行錯誤しながら作っていく過程を原稿にしてみます…」と伝えていました。ところが、いざ書き始めようとしたところで、具体的にはどんなものを作ってみようか?と迷ってしまいました。そこでまず、エンドユーザプログラミング環境って何だろう?といったところを考えてみることにします。
コンピュータは、誰かが考えたアイディアやアルゴリズムを、それを実現するプログラムに書き下して実行することにより、使うことができるものですね。コンピュータが登場した時点では、何かを目的としたプログラムを考えて作る人とそのプログラムのユーザは近かった(ほぼ同じだった)だろうと思われます。その後、入出力やプロセスの管理など、ほとんどの場合に必須となるような下層の基本的・定型的なプログラムはコンピュータを提供する側が作るようになり、ユーザは自身の目的に特化した部分のプログラミングに集中できるようになっていく、という流れでコンピュータの使われ方は進歩してきたはずです。
1980年代(前半ころ)の8bit CPUを積んだパソコンの多くは、電源を入れるとスタンドアロンBASIC環境が立ち上がります。ユーザはモニター画面に、ゲーム・家計簿など、それぞれの目的に応じたBASICのプログラムを入力し、それを実行して使います。バグがあればプログラムを修正し、出来上がったプログラムやデータはテープやフロッピーディスクなどの記録メディアに保存しておけば、次にパソコンを立ち上げたとき、再度プログラムやデータを入力するかわりにロードして再利用することができます。
このようなスタンドアロンBASIC環境パソコンでは、最初に立ち上がるのがBASICのプログラムを入力する画面であるといったところからして、ユーザ自身が自分の利用するプログラムを作ることを前提としています。この場合、BASICによるプログラミングもユーザインターフェースの一種と言えるでしょう。
一方、MacintoshやWindowsでは、ユーザ自身がプログラムするという前提は大きく後退して、ユーザはすでに完成しているアプリケーションプログラムを利用するというのが基本的な使い方ということなってきました。今日、多くの人がパソコンをどんなことに使っているかといえば、ウェブやメールや音楽や写真やワープロであったりするでしょうから、それで十分といえば十分なのかもしれません。
しかし、本来コンピュータは、メモリと速度と入出力の制限を抜きにすれば、どのようにもプログラミング可能で無限といってもいい可能性を秘めているはずです。ユーザの立場であっても、その万能性をもう少し活かさないことには少々もったいない感じがしてしまうのですがいかがでしょうか?
というわけで、ここでは「エンドユーザプログラミング環境」という言葉を「コンピュータの可能性を引き出す手段としてプログラミングをユーザインターフェースの一種として持っている環境」というような意味で使っています。
(補足: よく調べずに思いつきでこの言葉を使っていたのですが、ちょっと気になってググってみたところ、それほど多くはありませんがそれなりに使われているようです)
漠然とエンドユーザプログラミング環境について書いてきましたが、冒頭の「具体的にはどんなものを作ってみようか?」にもどります。既存のエンドユーザプログラミング環境として、私が思い浮かべるものとしては:
- 前述のスタンドアロンBASIC環境
- HyperCard
- Emacs
- Excel
- Squeak
- Mac OS X に付属している Automator や QuartzComposer
などがあります。
この中で私がよく使っているのは Emacs です。Emacs って何?と聞かれたときには「テキストエディタ」と答えるのがおそらく無難なのですが、実際にEmacsをよく使っている人にとって、単純にそう答えることにはどこかしら抵抗があるかもしれません。Emacs を起ち上げると、scratch (「なぐり書き」というような意味)と呼ばれる画面が表示されます。これはスタンドアロンBASIC環境での起動画面と同じように、Emacs上で動くプログラムを走り書き・試し書きするための画面にほかなりません。こういったことからも、少なくとも私にとって、Emacsはエディタであるとともに「エンドユーザプログラミング環境」としての意味が大きいわけです。
さて、上に列挙したあたりのものを参考に、どんなものを作るか考えているのですが、今のところは、Emacs、HyperCard、Excel のいずれかっぽいもの、あるいはそれらを混ぜたような感じなどを漠然と考えています。趣向としてはHyperCard路線がおもしろそうな気がしますが、Emacs路線にMac的な味付けをしてみたい気もするし…いずれにせよ、そんなにすごいものは作れないと思うのですが、迷いつつ次回に続きます。