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 パターンでした。

0 件のコメント :

コメントを投稿