頻出単語を数える

タグクラウドが流行のようで。

デザインパターンは、ちょっと離れて。そういえば、ダイナミック Objective-C今回からデザインパターンのお話しになっていますね。Objective-C というか、Cocoa のフレームワークではふんだんにデザインパターンが使われています。日本語訳されている Cocoa Fundamentals Guide では、Cocoa のデザインパターンについて書かれている部分があります。ダイナミック Objective-C の初回は、Singleton か...。AppleScript でできるのかな...。

そういえば...と、関係のない話が続きますが、iTunes。iTunes 7.0.1 を使っているのですが、iTunes 環境設定の「一般」で「ビデオを再生:」を別ウィンドウにしているとビデオを再生したときに別のウィンドウが開かれますね。このウィンドウを開いたままでライブラリの中の音楽を再生すると、アートワークが表示されるのですね。曲が変わるごとにウィンドウのタイトルが曲名になってアートワークが変わる...。こんな機能ありましたっけ?ちょっと便利。

Monzai って、日本語文字列の解析ができるのですね。品詞が取得できる。英語なんかだと単語の出現頻度を調べるのは簡単なのですが、単純に AppleScript で words ってすると日本語は助詞でもなんでも単語に見なされてしまう。それでも構わないのですが、タグクラウドを作ったときに「は」や「を」などがクローズアップされてしまう。それは、困る。かといって、面倒なことはしたくない。そこで、Monzai を使う。

以下のスクリプトは、要 Monzai です。とにかく、試してみたかっただけなので、コードは殴り書き。

Script Editor で開く

set str to the clipboard as Unicode text

tell application "monzai"
    set theResult to kaiseki controller 1 mode 1 moji str
end tell

set wordsList to {}
set wordsCount to {}
repeat with thisItem in theResult
    set tmp to original of thisItem as string
    if hinsicode of thisItem is 0 then
        if wordsList contains tmp then
            repeat with i from 1 to count wordsList
                if (item i of wordsList) is tmp then exit repeat
            end repeat

            set num to (item i of wordsCount) + 1
            set item i of wordsCount to num
        else
            set end of wordsList to tmp
            set end of wordsCount to 1
        end if
    end if
end repeat

set min to 1000000
set max to -1000000
repeat with thisItem in wordsCount
    if thisItem > max then set max to thisItem as integer
    if thisItem < min then set min to thisItem as integer
end repeat
{min, max}

set ratio to 18.0 / (max - min)
set cloud to {}

tell application "TextEdit"
    activate
    close documents saving no
    set theDocument to make new document
end tell

repeat with i from 1 to count wordsList
    set thisWord to item i of wordsList
    set num to item i of wordsCount
    set fs to (9 + (num * ratio)) as integer
    tell application "TextEdit"
        tell theDocument
            make new word at end of words of it with data "[ " with properties {size:fs, color:{0, 0, 0}}
            make new word at end of words of it with data thisWord with properties {size:fs, color:{0, 32896, 65535}}
            make new word at end of words of it with data " ] " with properties {size:fs, color:{0, 0, 0}}
        end tell
    end tell
end repeat

クリップボードの中にある文字列の中から名詞を抜き出し、TextEdit で複数回使われている名詞は大きく、そうでない名詞は小さく表示します。面白い、面白い。これで、星の数ほどあるブログを全部読まなくても要点が分かる...かな。

AppleScript とデザインパターン (6)

Factory Method の続きです。前回の最後の方に Factory(SimpleFactory) の例として以前に掲示板の方でご指摘いただいたコードを掲載しました。そのコードを Factory Method で書き直すとどうなるのでしょうか?

これがなかなか頭が痛い問題でして...。Factory Method で書き直してはいたのです。が、掲載を見送ったのでした。納得できなくて(というか、よく分からなくて)。とりあえず、もう一度、Factory Method パターンについて。

Factory Method パターンは、オブジェクトの生成を直接に行わず、オブジェクトの生成処理とそのオブジェクトを使用するものとの分離を行い、再利用性を高めます。AppleScript ではオブジェクトの生成というとハンドラ呼び出しがそれに当たります(もしくは、load script 命令による読み込み。copy 命令は...複製ですよね)。例えば、以下のような感じです。

Script Editor で開く

on TextEdit()
    script TextEdit
        on selectedText()
            tell application "TextEdit"
                if not (documents exists) then return ""
                activate
            end tell

            set tmp to the clipboard

            tell application "System Events"
                tell process "textedit"
                    keystroke "c" using command down
                end tell
            end tell

            if (the clipboard) is tmp then return ""

            return the clipboard as Unicode text
        end selectedText
    end script
end TextEdit

on KEdit()
    script KEdit
        on selectedText()
            tell application "KEdit"
                selected text of front document
            end tell
        end selectedText
    end script
end KEdit

-- client
on run
    set theEditor to TextEdit()
    tell theEditor to selectedText()
end run

使用者(run ハンドラ)は、オブジェクトの生成を直接行っています。この場合に困るのは生成するオブジェクトを変更した時。使用者はこの部分を変更しなくてはいけません。生成処理が変更されたら使用者にも影響が出ます。オブジェクトを使う方としてはこのような影響は受けたくありません。そこで、生成の処理をハンドラにしてしまいます。

Script Editor で開く

script SimpleFactory
    on getEditor()
        tell application "System Events"
            set a to ((name of processes whose frontmost is true) as Unicode text)
        end tell

        if a is "TextEdit" then
            return TextEdit()
        else if a is "KEdit" then
            return KEdit()
        else
            return missing value
        end if
    end getEditor
end script

-- client
on run
    tell application "TextEdit" to activate
    set theEditor to SimpleFactory's getEditor()
    if theEditor is missing value then return missing value

    tell theEditor to selectedText()
end run

こうしておくと生成処理に変更が加えられても使用者は気にすることなく利用できます。これが、Factory パターン(SimpleFactory ともいう)。これだけでもかなりの恩恵があります。では、Factory と Factory Method の違いは?

Factory は、オブジェクトの生成処理とどのオブジェクトを生成するかという判断も使用者から隠します。Factory Method パターンでは生成するオブジェクトごとに工場を用意し、ひとつの工場でひとつのオブジェクトを生成します。生成されるオブジェクトがどんなオブジェクトかということは使用者からは隠されますが、どのオブジェクトを生成するかという判断は使用者に委ねられます。生成するオブジェクトを変更したい場合、工場を持っているオブジェクトを切り替える必要があります。この辺りはできれば実行時に判断してオブジェクトを変更して欲しいものです。そのため、なんらかの仕掛けが必要になります。例えば、文字列からオブジェクトを生成するとか。

単純だけど分かりやすい SimpleFactory を使った方が無理がありません。この辺りは、ケースバイケース。Factory Method パターンは、大量のオブジェクトを生成する必要があるときや一緒に使って欲しいオブジェクトがある時などに利用できるパターンだそうで、あるオブジェクトとあるオブジェクトを一緒に使うということがはっきり分かっている時以外は適用しない方がいい、との注意もあります。それぞれのオブジェクトが単独で使えるような場合、なんでそのオブジェクトでオブジェクトを生成しているのかということが分かりにくくなるからです。

もしかしたら、Factory Method ではなく、Abstract Factory パターンの方がしっくりくるのかもしれません。まぁ、それは Abstract Factory の時のお話。では、引っ張りましたが Factory Method パターンを使って書き直したスクリプトを。

まず、生成されるオブジェクトをスクリプトファイルで保存します。

Script Editor で開く

on TextEdit()
    script TextEdit

        on selectedText()
            tell application "TextEdit"
                if not (documents exists) then return ""
                activate
            end tell

            set the clipboard to ""

            tell application "System Events"
                tell process "textedit"
                    keystroke "c" using command down
                end tell
            end tell
            return the clipboard as Unicode text
        end selectedText
    end script
end TextEdit

on init()
    return TextEdit()
end init

これを TextEdit.scpt として保存。init() は必ず実装します。以下のスクリプトを KEdit.scpt として保存。

Script Editor で開く

on KEdit()
    script KEdit

        on selectedText()
            tell application "KEdit"
                selected text of front document
            end tell
        end selectedText
    end script
end KEdit

on init()
    return KEdit(application "KEdit")
end init

オブジェクトを生成する工場を持つ以下のスクリプトを保存。

Script Editor で開く

on EditorEx(theEditor)
    script EditorEx
        property editor : theEditor

        on selectedText()
            set theEditor to my editorFacorty()
            if theEditor is missing value then return missing value
            tell theEditor to selectedText()
        end selectedText

        on editorFacorty()
            set appName to short name of (info for (path to editor))
            set fileName to appName & ".scpt"
            set scptFolder to path to scripts folder from user domain as Unicode text
            set scptFolder to scptFolder & "Applications:ExtraEditor:"
            set scptFile to scptFolder & fileName as Unicode text
            try
                return init() of (load script file scptFile)
            on error eMessage number eNumber
                return missing value
            end try
        end editorFacorty
    end script
end EditorEx

on init(theEditor)
    EditorEx(theEditor)
end init

ここでは、Editor.scpt という名前で保存しました。これらのファイルを ~/Library/Scripts/Applications/ に ExtraEditor というフォルダを作り、この中に保存します(もちろん、どこでもいいのですが)。Applications の中に保存するのは、スクリプトメニューに表示されないからです(ExtraEditor というアプリケーションがない限り)。

使用者は以下のような感じで呼び出します。

Script Editor で開く

on run
    tell application "System Events" to set f to name of (path to frontmost application)

    set scptFolder to path to scripts folder from user domain as Unicode text
    set scptFolder to scptFolder & "Applications:ExtraEditor:"
    set scptFile to scptFolder & "Editor.scpt"
    -- 初期化
    set theEditor to init(application f) of (load script file scptFile)
    -- 選択テキストを取得
    set str to selectedText() of theEditor
    if str is missing value then return
    display dialog str
end run

対象のエディタを指定して初期化します。ここでは、前面にあるアプリケーションの名前で初期化しています。前面のアプリケーションが対象のエディタでなかった場合、missing value が返ってきます。スクリプトメニューなどから実行してみてください。あとは、勝手にオブジェクトが判断するのでらくちん。バンドル形式にしてバンドルの中に全部のスクリプトを入れても構いません(その場合、修正が必要です)。

これなら対応エディタの数が増えても使用者の方で変更はいらないですね、と作った後に気がつく。オブジェクトを作成する方も変更がいらないですし。ひとつの工場がひとつのオブジェクトを作成するという部分もあっていますし、と自画自賛。いや、もうここまで作るのにどれだけの無駄スクリプトを書いたことか。累々たるってところです。ちなみに、今回掲載したスクリプトは、最後の最後で全面的に変更したものです。以前のものは...見れたものではなかったり。

ところで、short name って AppleScript 1.10 からでしたっけ?なら、Mac OS 10.4 以前では動かないですね。

若干、強引な感じもなきにしもあらずですが...こんなところでいかがでしょうか?

追記 - 06/09/29

掲示板の方でご指摘をいただきました。これ、よくよく見ると editorFacorty() ハンドラだけでいいですね。パターンにとらわれ過ぎていました。自戒の意味も込めて修正せずにこのままにしておきます。

AppleScript とデザインパターン (5)

次!

Factory Method!

って、なにも「!」をつけなくてもいいのですが。『ペアになるオブジェクトを生成するメソッド(ハンドラ)』ですね。

あるオブジェクトとあるオブジェクトが密接な関係にあり、対で使う必要があるという状況。これらのオブジェクトを別々に生成し、利用しているとオブジェクトの数が増えるに従ってどれとどれが一緒に使う必要のあるオブジェクトか分からなくなってきます。それならば、対となるべきオブジェクトを生成するメソッドを自身に持たせておこう、という考え方のデザインパターンです。

例えば、Iterator の時に Stack の対となる StackIterator を作りました。以下に再掲(行数稼ぎにあらず)。

Script Editor で開く

on Iterator(theObject)
    script Iterator
        property collectionObject : theObject
        property itemIndex : 1

        on hasNext()
            return (my itemIndex) is less than or equal to (getLength() of my collectionObject)
        end hasNext

        on nextItem()
            set num to itemIndex
            set itemIndex to itemIndex + 1
            return (getItemAt(num) of my collectionObject)
        end nextItem

        on resetIndex()
            set itemIndex of me to 1
        end resetIndex
    end script
end Iterator

on StackIterator(theObject)
    script StackIterator
        property parent : Iterator(theObject)

        on nextItem()
            return pop() of my collectionObject
        end nextItem
    end script
end StackIterator

on DataStructure()
    script DataStructure
        property theCollection : {}

        on pop()
        end pop

        on push(thisItem)
            set end of my theCollection to thisItem
        end push

        on getLength()
            return count my theCollection
        end getLength

        on getItemAt(num)
            return item num of theCollection
        end getItemAt

        on getIterator()
            return Iterator(a reference to me)
        end getIterator
    end script
end DataStructure

on Stack()
    script Stack
        property parent : DataStructure()

        on pop()
            if (my theCollection) is {} then return missing value

            set tmp to last item of my theCollection
            set my theCollection to reverse of (rest of (reverse of my theCollection))
            return tmp
        end pop

        on getIterator()
            return StackIterator(a reference to me)
        end getIterator
    end script
end Stack

on run
    set dataList to Stack()
    tell dataList
        push(10)
        push(20)
        push(30)
        push(40)
    end tell

    set theIterator to getIterator() of dataList
    theIterator's resetIndex()
    repeat while hasNext() of theIterator
        set thisItem to nextItem() of theIterator
        log thisItem
    end repeat
end run

Stack の getIterator() を使うことで一緒に利用するオブジェクトを返してくれます(そのオブジェクトがどんなものかは使う方には分からない)。これを getIterator() を使わないように書き直してみると以下のようになります。

Script Editor で開く

on run
    set dataList to Stack()
    tell dataList
        push(10)
        push(20)
        push(30)
        push(40)
    end tell

    set theIterator to StackIterator(dataList) -- *
    theIterator's resetIndex()
    repeat while hasNext() of theIterator
        set thisItem to nextItem() of theIterator
        log thisItem
    end repeat
end run

コメントしている部分が書き直した部分です。getIterator() を使わない場合、ここが問題となります。Stack ではなく他のデータ構造を使おうとしたとき、この部分の修正も行わなければならないのです。それぐらいのこと、と思われるかもしれませんが複数のファイルにスクリプトを分割していると全て修正しないといけないとなれば面倒です。

getIterator() という Iterator オブジェクトを作って返すメソッドを使っている限り、利用する集合オブジェクトを変更しても修正は必要なくなります。

このように一緒に使って欲しい、対となるオブジェクトを生成するメソッド(ハンドラ)をオブジェクトの中に持たせるというのが Factory Method パターンです。Factory Method パターンの各オブジェクトの関係は、次のようになります。

Product(生成物) と Creator(創作者) があります。Product が対になるオブジェクトで Creator が Product を生成するハンドラを持っています。先の Iterator で言えば、StackIterator が Product にあたり、Stack が Creator になります。Creator は、Product を返す factoryMethod を実装します。getIterator() が factoryMethod になります。

この Factory Method パターンをシンプルにしたものに Factory パターンというものがあります。以前、『問題』というコンテンツを書いたとき、提示した問題に対して掲示板の方でお答えを頂きました。そのときの答えが Factory パターンでした。以下に再掲します(再掲ばっかり)。

Script Editor で開く

script QuoEdit
    on selectedText()
        tell application "QuoEdit"
            text of selection
        end tell
    end selectedText
end script

script CotEditor
    on selectedText()
        tell application "CotEditor"
            contents of selection
        end tell
    end selectedText
end script

script KEdit
    on selectedText()
        tell application "KEdit"
            selected text of front document
        end tell
    end selectedText
end script

on getEnhanceEditor()
    tell application "System Events"
        set apps to ((name of processes whose frontmost is true) as Unicode text)
    end tell

    if apps is "QuoEdit" then
        return QuoEdit
    else if apps is "CotEditor" then
        return CotEditor
    else if apps is "KEdit" then
        return KEdit
    else
        return ""
    end if
end getEnhanceEditor

on run
    set EnhanceEditor to getEnhanceEditor()
    display dialog (EnhanceEditor's selectedText())
end run

『KEdit、QuoEdit、CotEditor のいずれが最前面にあっても選択している文字列は、同じ方法で取得したい』というのが問題でした。上記のスクリプトではいずれのエディタであっても適切なオブジェクトを返し、利用者はそれがどのオブジェクトかを気にせずに選択している文字列が取得できるようになっています。

getEnhancedEditor() がオブジェクトを生成(してはいないけど)する工場の役割を担っています。Factory パターンでは、工場の中でどのオブジェクトを返すかを動的に判断します。どのオブジェクトを返すかという処理も利用者から隠しています。Factory Method よりも Factory パターンの方が実装が楽なのでよく利用されるようです。

しかし、この方法ではオブジェクトの数が増えるに従って if 文の条件判断が増えていき、オブジェクトの精製処理が複雑になるに従って工場内での手続きが煩雑になります。Factory パターンの考え方をさらに進めた Factory Method パターンは、これらの欠点をある程度やわらげてくれます。

しかし、抽象クラスやインターフェース、クラスメソッドや final を使えない AppleScript ではいまいち説明がしにくい...。Factory Method で説明のしにくさを感じるのですから、残りのデザインパターンではどうなることなのやら。これらが使えないと「なぜ、そうするのか」ということが非常に分かりにくいですね。興味のある方は『増補改訂版Java言語で学ぶデザインパターン入門』や『Javaデザインパターン徹底攻略 (標準プログラマーズライブラリ)』や『オブジェクト指向における再利用のためのデザインパターン』を読んでみるといいかもしれません。

AppleScript とデザインパターン (4)

Iterator。

微妙。AppleScript にとって Iterator パターンって微妙。そもそも必要なのでしょうか。AppleScript は、大概の場合処理するデータをアプリケーションが持っています。そして、それらを取得して繰り返しで処理を行います。

Script Editor で開く

tell application "Finder"
    set curSelection to selection
    if curSelection is {} then return

    repeat with thisItem in curSelection
        if (class of thisItem is document file) and (name extension of thisItem is "scpt") then
            open thisItem
        end if
    end repeat
end tell

こんな感じ。ある意味、型が決まっているともいえます。アプリケーションがデータを持っていてそれを処理する AppleScript では、自分でデータ構造を作るということがあまりないです。Iterator パターンは『様々な集合オブジェクトに同一のアクセス方法を提供する』デザインパターンです。ある人が独自のデータ構造を作ったとします。他の人がこのデータ構造を走査したいという時に「Iterator を使ってね」と言われれば、そのデータ構造がどんなものか分からなくてもデータを走査することができるようになります。

データ構造の中身が分からなくても Iterator を使えばデータの処理ができる、というのが Iterator パターンの目的なのですが、AppleScript でこういう状況に出会うことがない。スクリプトを分割したり、大規模な開発でも行えばそうではないのかもしれないですが(そもそも AppleScript でそんな開発も少ないし、分担で開発も少ない)。

ま、ともかく。以下のような疑似コードではイケナイようですから、AppleScript でも上記の repeat の書き方はイケナイのでしょう。

for (i = 0 ;i < SIZE ;i++) {
    item = array[i];
}

これが駄目なのは、array の内部構造を意識しないといけないからですって。奥さん。

駄目と言われても...例えば、スクリプトを分割したとします。データを持っているスクリプトとそのデータを繰り返しで処理するスクリプト。データを持っている方のデータ構造を変更したら、繰り返しで処理する方も変更しないといけません。上記の疑似コードが通用するのはプログラムを分割しないときだけです。...ということのようです。だから、どんなデータ構造でも同じ方法でアクセスできる方法が必要になります、と。

前置きはこれぐらいにして。Iterator パターンでは、集合を表すオブジェクト(Aggregate = 集合、集合体)と集合の各要素に順番にアクセスするオブジェクト(Iterator = 繰り返し)が必要になります。集合を表すオブジェクトではなんらかのデータの集合を保持し、自身の各要素にアクセスするための Iterator オブジェクトを返すハンドラが実装されます。

Iterator オブジェクトでは、次の要素があるかどうかを返す hasNext() というハンドラと、次の要素を取り出す next() というハンドラが実装されます。

集合オブジェクトから Iterator オブジェクトを取得し、このオブジェクトの hasNext() で要素があるかどうかを調べ、next() で次の要素を取り出します。それがどのようなデータ構造でもこの手順でデータが取り出せるってスンポー。

まず、データの集合(Aggregate)となるオブジェクトを。

Script Editor で開く

on DataStructure()
    script DataStructure
        property theCollection : {}

        on pop()
        end pop

        on push(thisItem)
            set end of my theCollection to thisItem
        end push

        on getLength()
            return count my theCollection
        end getLength

        on getItemAt(num)
            return item num of theCollection
        end getItemAt

        on getIterator()
            return Iterator(a reference to me)
        end getIterator
    end script
end DataStructure

on Stack()
    script Stack
        property parent : DataStructure()

        on pop()
            if (my theCollection) is {} then return missing value

            set tmp to last item of my theCollection
            set my theCollection to reverse of (rest of (reverse of my theCollection))
            return tmp
        end pop
    end script
end Stack

on Queue()
    script Queue
        property parent : DataStructure()

        on pop()
            if (my theCollection) is {} then return missing value

            set tmp to first item of my theCollection
            set my theCollection to rest of my theCollection
            return tmp
        end pop
    end script
end Queue

スタックとキューで試してみます。どちらでも共通するハンドラを DataStructure で定義して Stack と Queue ではデータを取り出す部分だけを上書きしています。これらのデータ構造に順番にアクセスするための Iterator オブジェクトを返すハンドラは DataStructure で定義しています。

Iterator は、以下のようになります。

Script Editor で開く

on Iterator(theObject)
    script Iterator
        property collectionObject : theObject
        property itemIndex : 1

        on hasNext()
            return (my itemIndex) is less than or equal to (getLength() of my collectionObject)
        end hasNext

        on nextItem()
            set num to itemIndex
            set itemIndex to itemIndex + 1
            return (getItemAt(num) of my collectionObject)
        end nextItem

        on resetIndex()
            set itemIndex of me to 1
        end resetIndex
    end script
end Iterator

引数に集合オブジェクトをとります。集合オブジェクトに順番にアクセスするため、集合オブジェクトの現在の要素の位置を属性で持っています。AppleScript の集合の要素は 1 から始まるので、初期値は 1 です。hasNext() では、集合オブジェクトに次の要素があるかどうかを調べます。nextItem() で実際に次の要素を取り出します。

この両方のハンドラで集合オブジェクトの要素数や特定の位置の要素を取り出す必要があるので DataStructure ではその辺りのハンドラも実装しています。resetIndex() は、要素の開始位置を初期値に戻すハンドラです。

そして、以下のように使います。

Script Editor で開く

on run
    set dataList to Stack() -- or Queue()
    tell dataList
        push(10)
        push(20)
        push(30)
        push(40)
    end tell

    set theIterator to getIterator() of dataList
    theIterator's resetIndex()
    repeat while hasNext() of theIterator
        set thisItem to nextItem() of theIterator
        log thisItem
    end repeat
end run

--> 結果
(*10*)
(*20*)
(*30*)
(*40*)

キューであってもスタックであっても同じ方法でデータの最初から最後までを走査できています。

ええ、分かっています。スタックでデータが最初から表示されたらおかしいじゃないか、ということですね。Iterator は、全てのデータを走査するための方法を提供しているだけなので構わないと言えば構わないのですが。スタックのように最後に入れたものが最初に取り出されるようにするには、Iterator を継承して nextItem() を上書きし、Stack の方も getIterator() を上書きします。

Script Editor で開く

on StackIterator(theObject)
    script StackIterator
        property parent : Iterator(theObject)

        on nextItem()
            return pop() of my collectionObject
        end nextItem
    end script
end StackIterator

on Stack()
    script Stack
        property parent : DataStructure()

        on pop()
            if (my theCollection) is {} then return missing value

            set tmp to last item of my theCollection
            set my theCollection to reverse of (rest of (reverse of my theCollection))
            return tmp
        end pop

        on getIterator()
            return StackIterator(a reference to me)
        end getIterator
    end script
end Stack

StackIterator の nextItem() で pop() を実行しています。Stack の方では getIterator() で StackIterator を返すようにしてます。実行部分は修正する必要がないのでこのままで。実行すると逆順で要素が返ってきます。

...で。

何だ、「で」って。いえ、もう少し使い道のあるサンプルでも作ろうかと思ったのですが...思いつきませんでした。メッセージキューなんかだったら AppleScript でも使い道あるかな。

そんなわけで...Iterator パターンでした。

AppleScript とデザインパターン (3)

前回、Observer パターンをアプリケーションオブジェクトを使って試してみました。

なんで、初回が Observer パターンなのか。『PHP Hacks』で最初に取り上げられていたからというだけで、他に理由はありません。しかし...iPod ゲームはいけませんね。駄目です。とてもではないですが、暇つぶしで終わらせることなんてできません。全部買ってしまいそうな勢いです。

それはさておき...。前回は極力難しいことを考えずに Observer パターンを適用しました。デザインパターンの本や Web サイトを見ていると UML 図やパターンの概念的なコードがあります。スクリプトオブジェクトで概念的なコードを書いて一応 AppleScript でもデザインパターンを適用できるよ、という感じで進めていこうと思っていたのですが、概念的なコードってあくまで理解するためのものなのでどういうふうに使えばいいかがよく分からない。で、前回のようなスクリプトを書いたのですが...UML 図はともかく、一応動作を理解できるようにデザインパターンの概念的なスクリプトも書いておきます。

監視されるオブジェクトは、誰が監視しているかを知っています。変更があったときに通知を送るためにです。監視するオブジェクトを追加、削除できるような機能を持っています。監視オブジェクトを複数保持することができるようになっていて、それらに一括で通知を送る機能も持っています。

監視している方も監視対象を知っています。監視する方では必ず update() ハンドラを実装していました。監視されているオブジェクトは監視者の update() ハンドラを呼び出す、という取り決めの上に関係が成り立っているからです。

これらをスクリプトオブジェクトで表現すると以下のようになります。

Script Editor で開く

-- 監視オブジェクト
on Observer()
    script Observer
        -- 通知を受け取るハンドラ
        on update()
            log "update"
        end update
    end script
end Observer

-- 監視対象オブジェクト
on Observable()
    script Observable
        -- 監視オブジェクトのリスト
        property observers : {}

        -- 監視オブジェクトを加える
        on addObserver(observerObject)
            set end of observers to observerObject
        end addObserver

        -- 監視オブジェクトを削除する
        on deleteObserver(observerObject)
            repeat with thisObject in observers
                if (contents of thisObject) is observerObject then
                    set contents of thisObject to missing value
                    exit repeat
                end if
            end repeat

            set observers to scripts of observers
        end deleteObserver

        -- 通知を送る
        on notifyObservers()
            repeat with thisObject in observers
                update() of thisObject
            end repeat
        end notifyObservers
    end script
end Observable

監視する方、される方、どちらも最低限の機能だけを実装しています。Observer パターンは、これらのオブジェクトを拡張することで実現します。

Observer オブジェクトは、update() ハンドラを持ってます。これは実装側で実際の処理を記述します(インターフェースのつもり)。Observable オブジェクトは、監視者を追加、削除するハンドラと通知を送るハンドラを持っています。こちらも実際の処理はこのオブジェクトを継承したオブジェクトで実装し、自身が持つべきデータ(属性)とそれらのデータを取得、設定するハンドラを追加します。

蛇足なのですが、deleteObserver() ハンドラの最後の部分で

set observers to scripts of observers

としています。これは、スクリプトオブジェクトの利用を考えているのでこのようにしていますが、監視者が他のオブジェクト(アプリケーション等)の場合は、このままでは動かないので適切に変更する必要があります(他にも問題はあるのですが)。

実際に利用するには以下のように監視する方、される方、どちらも目的に合った実装を行います。

Script Editor で開く

-- 監視者オブジェクトの実装
on ConcreteObserver1()
    script ConcreteObserver1
        property state : missing value
        property observableObject : missing value

        on setObject(theObject)
            set observableObject of me to theObject
        end setObject

        on getObject()
            return observableObject of me
        end getObject

        on update()
            set state of me to observableObject's getState()
            log "Observer 1 update."
        end update
    end script
end ConcreteObserver1

on ConcreteObserver2()
    script ConcreteObserver2
        property state : missing value
        property observableObject : missing value

        on setObject(theObject)
            set observableObject of me to theObject
        end setObject

        on getObject()
            return observableObject of me
        end getObject

        on update()
            set state of me to observableObject's getState()
            log "Observer 2 update."
        end update
    end script
end ConcreteObserver2

-- 監視対象オブジェクトの実装
on ConcreteObservable()
    script ConcreteObservable
        property parent : Observable()
        property state : missing value

        on getState()
            return state of me
        end getState

        on setState(theState)
            set state of me to theState
        end setState
    end script
end ConcreteObservable

-- クライアント
on run
    -- 監視対象の作成
    set subjectObject to ConcreteObservable()
    -- 監視者の作成
    set observerObject1 to ConcreteObserver1()
    set observerObject2 to ConcreteObserver2()
    -- 監視対象を監視者に登録
    tell observerObject1 to setObject(subjectObject)
    tell observerObject2 to setObject(subjectObject)

    tell subjectObject
        -- 監視者を登録
        addObserver(observerObject1)
        addObserver(observerObject2)
        -- 状態の変化
        setState(true)
        -- 通知を送る
        notifyObservers()
        -- 監視者を削除
        deleteObserver(observerObject1)
        log "Delete Observer 1"
        -- 状態の変化
        setState(true)
        -- 通知を送る
        notifyObservers()
    end tell
end run

-- 結果
(*Observer 1 update.*)
(*Observer 2 update.*)
(*Delete Observer 1*)
(*Observer 2 update.*)

Script Editor のイベントログで確認してみてください。監視オブジェクトは、update() ハンドラを実装しています。ここで処理を行ってもいいですし、他のスクリプトオブジェクトやアプリケーションに処理を渡してもいいです。ここでは監視対象を属性で保持していますが、これは update() ハンドラの引数で受け取るようにしても構いません(その方が監視オブジェクトはそのままで、他でも再利用しやすくなるかもしれません)。update() ハンドラさえあれば実装はどのようなものでも構いません。

監視対象は自分の属性を操作するハンドラを持っています。監視する方はこのハンドラを使って状態を調べます。監視オブジェクトの追加や削除、通知を送るハンドラは親で定義されているので、監視対象が実装する必要があるのは自身の属性とハンドラです。

基本的にこのような形になり、目的や状況に応じて Observer パターンの考え方を適用しつつスクリプトを記述します(デザインパターンはプログラム設計の指針のようなものなので必ずしもこう書かなくてはならないというものではありません)。

使い道はいろいろあると思いますが...AppleScript は、一度に一つの動作しか実行できないので通知を送り、監視オブジェクトが処理を終了するまで監視対象の処理が待ち状態になります。AppleScript Studio でなら、ボタンを押した、ビューが更新されたなどのイベントがあったときに他のオブジェクトに通知を送るような設計が可能です。が、先にも書いたように通知先で処理が滞るようなら Observer パターンの適用は考えた方がいいと思います。

AppleScript Studio ではなく、前回のようなアプリケーション形式のスクリプトなら ignoring application responses を使って処理を待たずに先に進むことができます。いろいろなことを考えるとデザインパターンは、アプリケーション形式のスクリプトをオブジェクトに見立てて(見立ててって AppleScript ではオブジェクトなのですが)それらの関係にデザインパターンを用いる方が使い勝手はいいかもしれません。

前回の iTunes の曲が変わったときになんらかの動作を行うというスクリプトを以上を踏まえて変更すると以下のようになります。

Script Editor で開く

-- 監視オブジェクトのリスト
property observers : {}
-- iTunes の現在の曲
property curTrack : missing value

-- 監視オブジェクトを加える
on addObserver(observerObject)
    set end of observers to observerObject
end addObserver

-- 監視オブジェクトを削除する
on deleteObserver(observerObject)
    repeat with thisObject in observers
        if (contents of thisObject) is observerObject then
            set contents of thisObject to missing value
            exit repeat
        end if
    end repeat

    set observers to strings of observers
end deleteObserver

-- 通知を送る
on notifyObservers(observableObject)
    repeat with thisObject in observers
        ignoring application responses
            tell application thisObject to update(observableObject)
        end ignoring
    end repeat
end notifyObservers

-- 属性の取得
on getState()
    return curTrack of me
end getState

-- 属性の設定
on setState(thisTrack)
    set curTrack of me to thisTrack
end setState

on idle
    tell application "System Events"
        set bool to (name of processes whose visible is true) contains "iTunes"
    end tell

    if bool then
        tell application "iTunes"
            if player state is playing then
                set tmp to current track
            else
                set tmp to curTrack
            end if
        end tell

        if tmp is not curTrack then
            my setState(tmp) -- 状態の変更
            my notifyObservers(me) -- 通知を送る
        end if
    end if

    return 1
end idle

これを iTunesChecker という名前にして終了しないアプリケーションで保存します。監視者を追加するハンドラと通知を送るハンドラは変更しています。この部分は後で説明します。起動しても、まだ監視者がいないので曲が変わってもなんの反応もしません。

次に監視オブジェクトを作ります。以下は、Monzai を使った曲名の読み上げスクリプトです。

Script Editor で開く

on update(theObject)
    set theTrack to getState() of theObject
    if theTrack is missing value then return

    tell application "iTunes"
        set theArtist to artist of theTrack
        set theTitle to name of theTrack

        set msg to (theArtist & "の" & theTitle & "です") as Unicode text
    end tell

    tell application "monzai"
        speak controller 1 moji msg
    end tell
end update

update() ハンドラだけを持っています。これをアプリケーション形式で保存します。もう一つ、ダイアログで曲名を表示する監視オブジェクトを作ります。

Script Editor で開く

on update(theObject)
    set theTrack to getState() of theObject
    if theTrack is missing value then return

    tell application "iTunes"
        set theArtist to artist of theTrack
        set theTitle to name of theTrack

        set msg to (quote & theTitle & quote & return & return & theArtist) as Unicode text
    end tell

    tell application (path to frontmost application as Unicode text)
        activate
        display dialog msg buttons {"OK"} default button 1 giving up after 2 with icon 1 with title "Track Info"
    end tell
end update

こちらもアプリケーションで保存します。

最後にクライアントとなるべきスクリプトを作成します。監視対象と監視オブジェクトの操作はここで行います。

Script Editor で開く

on run
    tell application (path to frontmost application as Unicode text)
        activate
        display dialog "Add or Remove?" buttons {"Cancel", "Remove", "Add"} default button 3
        set theButton to button returned of result
    end tell

    set theApp to choose file of type {"APPL"} without invisibles
    set theInfo to info for theApp
    if (file creator of theInfo) is "aplt" then
        if theButton is "Add" then
            tell application "iTunesChecker" to addObserver(theApp as string)
        else
            tell application "iTunesChecker" to deleteObserver(theApp as string)
        end if
    end if
end run

実行し、先ほど保存した Monzai での読み上げアプリケーションか、ダイアログを表示するスクリプトを選択します。追加するとそのアプリケーションに通知が送られます。ここでは、監視オブジェクトのファイルパスを監視対象に渡しています。パスではなく、アプリケーションを渡せばいいのでは?と思います。先に『監視者を追加するハンドラと通知を送るハンドラは変更しています。この部分は後で説明します』と書きましたが、アプリケーションやスクリプトオブジェクトを監視対象に登録するのは問題があるのです。

なにが問題かといいますと...例えば、アプリケーションを登録するようにしておきます。

addObserver(application "Log")

アプリケーションを登録してから iTunesChecker を終了します。終了後もスクリプトオブジェクトの属性は保持されます。次回起動時も登録されたままなので面倒がありませんが、削除ができないのです。

deleteObserver(application "Log")

これが実行できない。なぜかというと、最初に登録したアプリケーションオブジェクトと削除しようとしているアプリケーションオブジェクトが別のものと判断されるからです。削除するには同じオブジェクトでないといけないのですが、iTunesChecker に渡して終了した時点で iTunesChecker で別のオブジェクトとして保存されるのです(同じスクリプトオブジェクト、同じアプリケーションオブジェクトと判定できるルーティンを組めばいいのですが、そうなると余分な処理に時間がかかる)。

AppleScript のスクリプトオブジェクトは、スクリプトの中に一つしか存在できません。copy 命令や load script 命令で同じスクリプトオブジェクトを複製、生成したとしてもオリジナルとは異なるオブジェクトになります。オブジェクトの独自性が保証されているのですが、同じものを渡すことができないような状況では困ることがあります。

だから、アプリケーションのパスを渡しているのです。これなら、同じ文字列なら同じ文字列と判定されるので。が、そのためにアプリケーションの場所を移動できないという制限が加わります(alias でも構わないのですが)。

あちらを立てればこちらが立たず...AppleScript ではこれが限界かな...?

デザインパターンを用いると必然的にコード量が増えるのですが、ここまでする必要があるのか?という疑問があると思います。もちろん、作るスクリプトによりけりなのですが、上記のスクリプトを一つにまとめてみることを考えるとメリットとデメリットが分かると思います。

iTunes で曲が変わったときに曲名を使う人の好みによりダイアログか声で知らせる。...面倒そうに思えます。

Observer パターンを使った方は、機能を追加したいときにその部分だけを作ればいいようになっています。iTunesChecker もクライアントも修正する必要がありません。update() ハンドラを持ったスクリプトアプリケーションを作るだけです。いい面、悪い面ありますが、こういう AppleScript もいいかもしれません。

AppleScript とデザインパターン (2)

「なんで iTunes で曲が変わったときに教えてくれないんだ?」

「iPod がマウントされたときに自動的にテキストを転送したいんだけど...」

「外付けの HDD をマウントしたときに勝手にバックアップしてくれよ」

「HTML を編集・保存したときにブラウザで勝手に再読み込みして欲しいんだけど」

「新着メールが来たら教えて欲しい」

...人間というのは結構わがままな生き物ですね。iTunes といえば、新しい iTunes 7。track クラスに動画関連の属性が追加されていますね。それ以外で大きな変更は見当たらず...つまり、今までのスクリプトはほとんどそのままで使えるということです。多分。

いや、iTunes 7 のことではなくて。先に上げたような要望を満たすには、idle ハンドラを使えばいいでしょう。例えば、iTunes で次の曲になったときに動作するようなスクリプトなら、以下のような感じ。

Script Editor で開く

property currentTrack : ""

on idle
    tell application "iTunes" to set tmp to name of current track

    if tmp is not currentTrack then
        tell application (path to frontmost application as Unicode text)
            activate
            display dialog tmp buttons {"OK"} default button 1 giving up after 2 with icon 1
        end tell

        set currentTrack to tmp
    end if

    return 1
end idle

これで曲が変わったときだけ教えてれますね。Dock に表示されて鬱陶しいというなら、スクリプトをアプリケーションバンドル形式で保存してプロパティリストをごにょごにょ...とすればいいでしょう。

AppleScript には「あるオブジェクトの状態が更新されたらそれを通知する」という機能がありません。まぁ、スクリプト言語なんだからそこまで必要ありませんが。あることが起きたかどうかを監視したいというとき、上記のようなスクリプトを作り、idle ハンドラで目的を達成するというのが基本的な方法ではないでしょうか。

と、ここまでくればなんのデザインパターンを取り上げようとしているのか分かるのではないでしょうか。あるオブジェクトの状態を監視し、更新されたことを通知してもらう Observer パターンです。

ほとんどの場合、上記のようなスクリプトで問題はなく、わざわざデザインパターンを用いることもないのですが...「AppleScript でこういうこともできるんだ」というぐらいに捉えていただければさいわいです。話のネタってやつです。

Observer パターンを簡単に説明。監視するオブジェクトと監視されるオブジェクトがあります。先のスクリプトを監視側と監視される側に分けてみましょう。監視されるオブジェクトは iTunes で曲が変わったかどうかを調べ、それを自分の属性(currentTrack 属性)として持ちます。

監視するオブジェクトは、監視対象のオブジェクトの状態を監視しています。ここでは、iTunes の曲が変わり、属性が更新されたかどうかです。

曲が変わり、currentTrack 属性が更新された時、監視されているオブジェクトは監視しているオブジェクトに更新があったことだけを伝えます。監視しているオブジェクトは通知を受け取るとなんらかの処理(ここではダイアログを表示しています)を実行します。

監視されているオブジェクトはデータを持っています。監視するオブジェクトはなんらかの処理を行います。Observer パターンでは、データと処理が分離され、お互いのことを詳しく知る必要がありません。このため、処理の追加と既存のコードの修正が容易になります。

先のスクリプトを監視と監視される側に分離してみます。まず、監視するスクリプト。

Script Editor で開く

on update(theObject)
    set theDate to getState() of theObject
    tell application (path to frontmost application as Unicode text)
        activate
        display dialog theDate buttons {"OK"} default button 1 giving up after 2 with icon 1
    end tell
end update

このスクリプトを Observer という名前でアプリケーション形式で保存します。update() ハンドラが監視しているオブジェクトの通知を受け取るハンドラで、監視しているオブジェクトを引数にとります。そして、監視しているオブジェクトの getState() ハンドラを使ってオブジェクトの状態を取り出します。

監視されるオブジェクトは、以下のようになります。

Script Editor で開く

property currentTrack : ""
property observerObject : application "Observer"

on getState()
    return currentTrack of me
end getState

on idle
    tell application "iTunes" to set tmp to name of current track

    if tmp is not currentTrack then
        set currentTrack to tmp
        ignoring application responses
            tell observerObject to update(a reference to me)
        end ignoring
    end if

    return 1
end idle

このスクリプトを Observable という名前で「実行後、自動的に終了しない」にチェックを入れ、アプリケーション形式で保存します。監視されるオブジェクトでは、監視しているオブジェクトに通知を送るために監視するオブジェクトを属性として持っています。また、監視しているオブジェクトが状態を取得できるように getState() ハンドラを定義しています。

このスクリプトを起動します。iTunes の曲が変わると Observer として保存したスクリプトアプリケーションに自分の参照を引数にして通知を送ります。Observer は通知を受け取ると Observable の状態を取得し、ダイアログを表示します。

...おお。頭の中だけで考えていたことだけど、ちゃんと動きますね。

このようにすることでなにが嬉しいか。そこが問題です。監視される方のオブジェクトは複数から監視されることがあります。今のサンプルでは監視側は一つです。複数の監視オブジェクトを保持するように変更しましょう。まず、監視するオブジェクトを作ります。

Script Editor で開く

on update(theObject)
    beep
end update

なんのことはなく、ビープするだけです。これを Beeper としてアプリケーション形式で保存します。観察されるオブジェクトは以下のように変更します。

Script Editor で開く

property currentTrack : ""
property observerObject : {application "Observer", application "Beeper"}

on getState()
    return currentTrack of me
end getState

on idle
    tell application "iTunes" to set tmp to name of current track

    if tmp is not currentTrack then
        set currentTrack to tmp
        repeat with thisObserver in observerObject
            ignoring application responses
                tell thisObserver to update(a reference to me)
            end ignoring
        end repeat
    end if

    return 1
end idle

複数の監視オブジェクトを持つように属性を変更し、繰り返しで通知を送っているだけです。監視側の処理を待たずに先に進むようにしているのでそれほ処理に手間取ることはないと思います。監視側も監視しているオブジェクトのことを気にすることなく勝手に動作しているので監視されているオブジェクトの idle ハンドラに影響が及ぶことはありません。これは、監視するオブジェクトを増やしているのですが、同時に機能を追加していることにもなります。

iTunes の状態を監視しているオブジェクトが監視されているオブジェクトで、そいつを監視するオブジェクトがいるというなんとも奇妙な関係ですが、iTunes で再生している曲が変わったときに教えてくれるようにハンドラを追加することができないので仕方がないです。本当は、iTunes にハンドラを追加する方がスマートな感じなのですが。しかも、観察されているオブジェクトが常時起動していて監視しているオブジェクトが変更時に呼び出されるだけですし。

今のままではダイアログが表示されるのでキーを打っているときに鬱陶しいのですが、Monzai や say コマンドを利用して発話させるようにすればもう少し使い勝手もあがるでしょう。

Script Editor で開く

on update(theObject)
    set theDate to getState() of theObject
    tell application "monzai"
        speak controller 1 moji theDate
    end tell
end update

さて。Observer は以上のような感じなのですが...思ったより長くなったので続きは、また今度。

AppleScript とデザインパターン (1)

PHP Hacks』という書籍があります。個人的にはとても楽しめた書籍でした。ソースコードは PHP 特有の分かりにくさがありますが。

PHP 特有の分かりにくさというのは、HTML の中の PHP(PHP が分かりにくいのではありません)。 という行が頻出するけど、これはだいぶ前にあった if の閉じ括弧なのか、if よりさらに前にあった for の閉じ括弧なのか...とかなり悩む。実際に書いてみても分からない。シンタックスカラーリング機能のあるエディタで書いてみてもこういうコードをまともに色付けしてくれなかったりする。

PHP と HTML は分けて欲しかった...。そう思うけど、そうするとページ数が足りなくなるか。

しかし、面白かった。iPhoto のライブラリにアクセスとか PSP の活用とか Jabberlivedoor Weather Hacks との組み合わせとか。ぱっと見て気を引く内容のものが多かった。Mac OS X になってなにが良かったかというと、PHP でも Perl でも C 言語でも Windows を対象に書かれている書籍でも十分役に立つということでしょうか(Windows の機能に依存しているものは駄目ですが)。『PHP Hacks』自体が Linux、Mac OS X、Windows を対象にしているし。

そして、この書籍の中に PHP でデザインパターンを使ってみよう、という章がありました。『オブジェクト指向における再利用のためのデザインパターン』です。23 種類全てのデザインパターンを試しているわけではないのですが、なかなか興味深かったです。

こういう刺激を受ける書籍を読んでいると、やりたくなるんですよね。AppleScript ではどうだろうか?と。

そんなわけで、AppleScript + デザインパターンにハマっていました。「だから、更新がなかったのか」と思ったあなた。鋭いです。

AppleScript でデザインパターン...。無理があると言われればそうなのですが、『オブジェクト指向における再利用のためのデザインパターン』がオブジェクト指向で汎用的に利用できるプログラム設計の方法論といった感じのもの(つまり、オブジェクト指向ならプログラミング言語を問わない)なので AppleScript でもスクリプトオブジェクトを使って恩恵を蒙ることはできます。Template Method や Factory、Strategy などは、使っていてとても気持ちいいです。特に同じようなことを異なるアプリケーションで実現することが多い(もしくは、同じようなコードを使い回すことが多い)AppleScript ではデザインパターンを使うメリットはあるのではないか?とも思いました。

ま、しかし。こういうことは実際に試してみないことにはなにも分かりません。ということで、唐突ですが、AppleScript でデザインパターン始めます。