オブジェクト指向AppleScript言語

今までAppleScriptに備わっているオブジェクト指向的な仕組みを、あまり積極的に利用していなかった...。アプリケーションの補助的な操作に利用することが多く、シンプルなスクリプトを手順に従って並べるだけで結構満足できていた、ということもある。それに何より、オブジェクト指向的に書く方法、もっと言えばAppleScript自体をあまり良く理解できていなかったというのもある。
いつも、その場限りの必要な知識だけ調べて、動いたらそれまで。試行錯誤のやっつけスクリプトだった。いい加減、ちゃんと理解しておきたい...。

Hello World

  • 「こんにちは」とダイアログで表示するだけの最もシンプルなコードだが、この裏には実に多くの仕組みが隠されていた。
display dialog "こんにちは"
on run
  display dialog "こんにちは"
end run
  • ちなみに、以下のような書き方をしてしまうと、エラーが発生して実行できない。(コンパイルも、保存もできない。)
    • 構文エラー
    • ハンドラ run が 2 度以上も指定されています。または、ハ
      ンドラ run と重複する命令がスクリプトの最上位にありま
      す。
display dialog "こんにちは"

on run
  display dialog "こんにちは"
end run
  • ここまでの理解:スクリプトエディタの実行ボタンを押した時は、そこで編集中のコードに対してrunハンドラ(メソッド)が呼び出される。

ハンドラ(メソッド)

  • スクリプトを実行する方法は、実は、スクリプトエディタの実行ボタンを押す以外にも様々な方法が用意されている。
ファイルのドロップで実行
  • 以下のコードをフォーマット:アプリケーションとして保存すれば...
on open
  display dialog "ファイルがドロップされました。"
end open
    • アイコンには以下のように矢印が表示されている。(openハンドラがない時は表示されない。)


    • そのアイコンにファイルをドロップしてみると、openハンドラ内のコードが実行された。
    • アイコンをダブルクリックで起動しても何も起こらない。
  • もし、runハンドラとして実行されるコードも用意されていれば...
display dialog "ダブルクリックされました。"

on open (aFile)
  display dialog "ファイルがドロップされました。" & (aFile as text)
end open
    • アイコンをダブルクリックすると、「ダブルクリックされました。」とダイアログが表示された。
    • ちなみに、ドロップされたファイルは引数として受け取ることができるのであった。
様々なハンドラ
  • フォーマット:アプリケーションで保存した時に利用できるハンドラ
display dialog "ダブルクリックされました。"

--ファイルがドロップされた時に実行される。
on open (aFile)
  display dialog "ファイルがドロップされました。" & (aFile as text)
end open

--アプリケーション実行中に、起動操作した時に実行される。
on reopen
  display dialog "Dockアイコンをクリック、またはアプリケーションアイコンがダブルクリックされました。"
end reopen

--アプリケーションが終了する直前に実行される。
on quit
  display dialog "終了します。"
end quit

--5秒毎に実行される。(オプション:実行後、自動的に終了しない チェックありの場合に有効)
on idle
  beep
  return 5 --次に実行するまでの秒数を返す
end idle
ユーザーが定義するハンドラ
  • ハンドラはあらかじめ用意されているもの以外にも、ユーザーが自由に定義することができる。
  • 定義したハンドラは、on以降のハンドラ名で呼び出すことができる。
  • もちろん、今までのopenハンドラだって同様に呼び出すことができる。
say_hello() --ユーザー定義のハンドラは、引数が不要でも括弧は必要
open

on say_hello() --ユーザー定義のハンドラは、引数が不要でも、括弧は必要
  display dialog "こんにちは"
end say_hello

on open
  display dialog "ファイルがドロップされました。"
end open
  • ここまでの理解:
    • ハンドラ(メソッド)を定義しておくことで、様々なメッセージ呼び出しに反応するスクリプトを作成することができる。
    • ハンドラ(メソッド)はスクリプトの中からハンドラ名で簡単に呼び出すことができる。

スクリプトオブジェクト

  • scriptブロックで囲むことで、そのscriptブロックが所有するコードのように振る舞う。(まるでscriptブロック名のオブジェクトのようだ。)
say_hello() of Sazae
say_hello() of Tara

script Sazae
  on say_hello()
    display dialog "こんにちは、サザエでございます。"
  end say_hello
end script

script Tara
  on say_hello()
    display dialog "こんにちは、タラです。"
  end say_hello
end script
  • 誰の(どのスクリプトの)ハンドラかを明示することで、同じハンドラ名でも区別して呼び出すことができるのだ。

継承

  • 継承を利用すると、タラちゃんにsay_hello()ハンドラは不要になる。(タラちゃんへのsay_hello()は、親であるサザエさんが肩代わりしてくれるので。)
say_hello() of Sazae
say_hello() of Tara

script Sazae
  on say_hello()
    display dialog "こんにちは、" & first_name() & "です。"
  end say_hello
  
  on first_name()
    "サザエ"
  end first_name
end script

script Tara
  property parent : Sazae --親は script Sazae
  
  on first_name()
    "タラ"
  end first_name
end script

自動継承

  • 実は、同じコード内のスクリプトオブジェクトは、トップレベルのスクリプトを自動的に継承するので、property parent無しで以下のようにも書ける。
say_hello() of Sazae
say_hello() of Tara

on say_hello()
  display dialog "こんにちは、" & first_name() & "です。"
end say_hello

script Sazae
  on first_name()
    "サザエ"
  end first_name
end script

script Tara
  on first_name()
    "タラ"
  end first_name
end script

継承されるプロパティの共有

  • 継承関係にあるオブジェクト間では、プロパティは共有される。
  • どちらのfirstNameを変更しても、変更後の結果はどちらから確認しても同じ値になっている。
script Sazae
  property firstName : "サザエ"
end script

script Tara
  property parent : Sazae --親は script Sazae
end script

set firstName of Sazae to "タラ"
firstName of Tara

--結果: "タラ"
script Sazae
  property firstName : "サザエ"
end script

script Tara
  property parent : Sazae --親は script Sazae
end script

set firstName of Tara to "タラ"
firstName of Sazae

--結果: "タラ"
  • ここまでの理解:スクリプトオブジェクトを直接指定した時、プロパティはクラス変数のように振る舞う。

スクリプトオブジェクトのsetによる代入

  • スクリプトオブジェクトはsetによって、変数に代入することができる。
  • setでscript Personが、変数sazaeに代入された場合...
    • 変数sazaeは、script Personそのものを指し示している。(変数sazae = script Person)
script Person
  property firstName : ""
  
  on say_hello()
    display dialog "こんにちは、" & firstName & "です。"
  end say_hello
  
  on set_firstName(aName)
    set firstName to aName
  end set_firstName
end script


set sazae to Person --sazaeにPersonを代入する
set_firstName("サザエ") of sazae

say_hello() of sazae
--ダイアログ: "こんにちは、サザエです。"

say_hello() of Person
--ダイアログ: "こんにちは、サザエです。"


set_firstName("xxxx") of Person

say_hello() of sazae
--ダイアログ: "こんにちは、xxxxです。"

say_hello() of Person
--ダイアログ: "こんにちは、xxxxです。"
  • ここまでの理解:上記のようにsetで代入した場合、Personクラスのように振る舞う。

スクリプトオブジェクトのcopyによる代入

  • スクリプトオブジェクトはcopyによって、変数に代入することができる。
  • copyでscript Personがコピーされて、変数sazaeに代入された場合...
    • 変数sazaeは、script Personのコピーであって、オリジナルではない。(変数sazae ≠ script Person)
script Person
  property firstName : ""
  
  on say_hello()
    display dialog "こんにちは、" & firstName & "です。"
  end say_hello
  
  on set_firstName(aName)
    set firstName to aName
  end set_firstName
end script


copy Person to sazae --personをコピーして、sazaeに代入する
set_firstName("サザエ") of sazae

say_hello() of sazae
--ダイアログ: "こんにちは、サザエです。"

say_hello() of Person
--ダイアログ: "こんにちは、です。"


set_firstName("xxxx") of Person

say_hello() of sazae
--ダイアログ: "こんにちは、サザエです。"

say_hello() of Person
--ダイアログ: "こんにちは、xxxxです。"
  • ここまでの理解:上記のようにcopyで代入した場合、Personインスタンスのように振る舞う。

スクリプトオブジェクトのコピー時の初期化

  • コピー(インスタンス化)する時の初期化は、結構面倒くさい。だから、初期化ハンドラを用意しておくと幸せになるかもしれない。
スクリプトオブジェクト内に初期化ハンドラを定義する
  • new(aName)of Personを実行すると、script Personのコピーが生成される。
  • script Personのオリジナルにもアクセス可能。
  • 自分でcopy Person to ...を利用してコピーすることも出来る。
script Person
  property firstName : ""
  
  on new(aName)
    copy me to aCopy --meは自分自身を表す(ここでは script Person になる)
    set_firstName(aName) of aCopy
    aCopy
  end new
  
  on say_hello()
    display dialog "こんにちは、" & firstName & "です。"
  end say_hello
  
  on set_firstName(aName)
    set firstName to aName
  end set_firstName
end script

set sazae to new("サザエ") of Person
set tara to new("タラ") of Person

say_hello() of sazae
--ダイアログ: "こんにちは、サザエです。"

say_hello() of tara
--ダイアログ: "こんにちは、タラです。"

say_hello() of Person
--ダイアログ: "こんにちは、です。"
初期化ハンドラで囲む
  • make_person(aName)を実行した時だけ、script Personのコピーが生成される。
  • script Personのオリジナルにはアクセスできない
  • copy Person to ...は利用できない
on make_person(aName)
  script Person
    property firstName : aName
    
    on say_hello()
      display dialog "こんにちは、" & firstName & "です。"
    end say_hello
    
    on set_firstName(aName)
      set firstName to aName
    end set_firstName
  end script
end make_person

set sazae to make_person("サザエ")
set tara to make_person("タラ")

say_hello() of sazae
--ダイアログ: "こんにちは、サザエです。"

say_hello() of tara
--ダイアログ: "こんにちは、タラです。"

say_hello() of Person
--AppleScriptエラー: "Person変数は定義されていません。"
クラスメソッドとインスタンスメソッドを明確にする
  • スクリプトオブジェクト内に初期化ハンドラnew()を定義してしまうと...
    • クラスとインスタンスのハンドラ(メソッド)が混じってしまい使い勝手が悪そう。
  • かといって、トップレベルの初期化ハンドラとして定義してしまっても...
    • Personオブジェクトとしてのまとまりがなくなってしまう...。
  • そこで、初期化ハンドラで囲んだ後、さらにscriptとして定義してしまう方法を思い付いた。
    • 外側のscript名をPersonに、内側のscript名を_Personとでもしておけば、クラスとインスタンスの関係をかなり表現できそう。
script Person
  on new(aName)
    script _Person
      property firstName : aName
      
      on say_hello()
        display dialog "こんにちは、" & firstName & "です。"
      end say_hello
      
      on set_firstName(aName)
        set firstName to aName
      end set_firstName
    end script
  end new
end script

set sazae to new("サザエ") of Person
set tara to new("タラ") of Person

say_hello() of sazae
 --ダイアログ: "こんにちは、サザエです。"

say_hello() of tara
 --ダイアログ: "こんにちは、タラです。"

say_hello() of Person
 --AppleScriptエラー: "«script Person» は say_hello メッセージを認識できません。"
 --say_hello()はインスタンス的ハンドラなので、Personクラスに対しては当然のエラー
ほとんどのoooo of xxxxは、xxxx's ooooに置き換え可能
  • これまでAppleScriptで象徴的な表現、oooo of xxxx of yyyyという書き方を続けてきた。
  • しかしこの表現方法だと、最後まで読まないと根本は何かを理解できないところが煩わしい。
  • 何より日本人の自分が自然と感じる感覚とは、言葉の入ってくる順番が反対なのだ...。
  • しかし、AppleScriptは柔軟な言語で、実は以下のようにも表現できたのであった。
...(中略)...
set sazae to Person's new("サザエ")
set tara to Person's new("タラ")

sazae's say_hello()
 --ダイアログ: "こんにちは、サザエです。"

tara's say_hello()
 --ダイアログ: "こんにちは、タラです。"

Person's say_hello()
 --AppleScriptエラー: "«script Person» は say_hello メッセージを認識できません。"
 --say_hello()はインスタンス的ハンドラなので、personクラスに対しては当然のエラー
  • つまり oooo of xxxx of yyyy = yyyy's xxxx's oooo なのである。
  • 「's」を「.」と置き換えて理解すれば、かなりRubyの表現に近づいた。

ここはどこ、私はだれ?

  • スクリプトオブジェクト内に初期化ハンドラnewを定義する時に、meというキーワードを利用した。
自分自身
  • meは自分自身を返すのだが、自分自身とは何だろうか?
me
 --結果: «script»
tell application "Finder"
  me
end tell
 --結果: «script»
script Sazae
  me
end script

Sazae run
 --結果: «script Sazae»
script Sazae
  who()
end script

on who()
  me
end who

Sazae run
 --結果: «script Sazae»

who()
 --結果: «script»
  • meは、«script» あるいは «script Sazae» を返している。
  • つまり、meを実行する環境のスクリプトオブジェクトを指し示しているのだ。
スクリプトオブジェクトのルーツ
  • では、«script»の親はいるのだろうか?そして、そのまた親は...。以下のように調べてみた。
me
 --1.結果: «script»

parent of me
 --2.結果: «script AppleScript»

parent of parent of me
 --3.結果: current application

parent of parent of parent of me
 --4.AppleScript エラー: &1 を取り出すことはできません。
2.結果:«script AppleScript»
  • «script AppleScript»は、スクリプトコードの中で AppleScript というキーワードで表現される。
  • AppleScript Language Guideには、AppleScript itself (the AppleScript component) と書かれている。
  • AppleScript自身(AppleScriptの構成要素)とは、何だろう?AppleScriptを解釈して実行するインタプリター(翻訳機)とか、言語環境そのものと考えれば良いのだろうか。
  • 日本語とは何だろうと置き換えて考えた時、まず日本語の文法を説明して、その文法を理解できる人間の心であり、その心が集まってコミュニケーションする環境であると言える。
3.結果:current application
  • current applicationは、ユーザーの操作を受け取って、その操作をAppleScriptでオブジェクトに伝えるアプリケーションと考えれば良いだろうか。
  • 以下のようなコードで試してみた。(フォーマット:スクリプト、ファイル名:me.scpt で保存した。)
display dialog (path to current application as text)
--osascriptコマンドで実行する場合、上記display dialog...はコメントアウトして保存
path to current application as text
  • ターミナルのosascriptコマンドから実行した場合
    • 返り値: "Leopard HD:usr:bin:osascript"
  • フォーマット:アプリケーションとしてme.appという名前で保存して実行した場合
    • ダイアログ: "Leopard HD:Usrs:zari:Library:Scripts:me.app"
  • me.scpt、あるいはme.appを起動する環境によって、current applicationは違っている。
  • つまり、current applicationは、ユーザーの操作によってAppleScriptを話してくれるオブジェクトと考えることが出来そう。
4.AppleScript エラー:&1 を取り出すことはできません。
  • ここでエラーが発生するということは、これ以上親を遡ることが出来ないということ。
  • me(自分自身)の元祖は、上記のcurrent applicationということになる。(OSレベルではさらに詳細な仕組みが隠されているかもしれないが。)

Hello Worldが表示されるまで

  • ここで最初に戻って、以下のコードがどのように実行されているか考え直してみる。
display dialog "こんにちは"
    1. ユーザーがスクリプトエディタの実行ボタンを押す。
    2. スクリプトエディタが実行ボタンを押されたことを感じ取ってrunメーセージをしゃべる。(OSレベルではさらに詳細な仕組みがあるはず)
    3. runメーッセージがトップレベスクリプトに届いて、runハンドラ内のコードが実行される。
    4. display dialog "こんにちは"が、トップレベスクリプトAppleScriptインタプリター、スクリプトエディタの順に伝播してダイアログ ウィンドウに「こんにちは」と表示される。
  • 以上のような仕組みであると(自分では)考えることにした。

そのメッセージは誰に向けられているか?

  • もし、以下のようにtellブロックで囲んだとすれば...
tell application "Finder"
  display dialog "こんにちは" with icon note
end tell
    • display dialog "こんにちは"...の送信先はFinder.appになる。
    • with icon noteオプションで表示されるアイコンもFinderの絵になっている。


    • ちなみに、tellブロックがない時はスクリプトエディタ(current application)のアイコンだった。

      • もし、スクリプトメニューやme.appからの起動であれば、そのcurrent applicationアイコンになると思う。
  • AppleScriptとは、オブジェクト(アプリケーションやスクリプトコード自身)間でメッセージをやり取りするための言語なのであった。
  • そのメッセージ(スクリプトコード)はどのオブジェクトに向けて送信されているのか、常に意識しておく必要がある。
  • 以前、以下のようなコードを書いて、frontmost_app()が認識されなくて困ったことがあったが、今ならその理由も簡単に理解できる。
--最前面のアプリケーションを取得する
on frontmost_app()
  tell application "System Events"
    set pList to name of every process whose frontmost is true
    item 1 of pList
  end tell
end frontmost_app

tell application "System Events"
  tell process frontmost_app()
...(中略)...
  end tell
end tell
  • tell application "System Events"ブロック内のため、frontmost_app()が"System Events"に向けて送信されていたのだ...。
  • frontmost_app()はトップレベスクリプトなのだから、my frontmost_app()とすれば、ちゃんと認識されるはずだ。


AppleScriptを理解するオブジェクトの気持ちが、少しだけ分かったような気になった。

参考ページ

  • AppleScript Language Guide
    • 英語の情報なのだが、何はともあれAppleScriptを利用するのであれば読んでおくべき。
    • 例え英語の解説が分からなくとも、豊富なサンプルコードの実行結果を確認しておくだけで、かなり理解が深まる。
    • ちなみに、自分は今まで英語だからといって敬遠していた。熟読したのは、今回が初めて。もっと早くに読んでおくべきだった...。

そして、上記ページの理解を深めるために、以下の素晴らしいサイトがたいへん参考になりました。感謝です!


トップレベスクリプトとrunハンドラ内のスクリプトの違い

  • 素敵なブクマコメントを頂いた。

「初期化ハンドラで囲む」でscript PersonのオリジナルにアクセスできないのはPersonがmake_personハンドラのローカルオブジェクトだから、だっけか。make_personハンドラが呼ばれてる間しか存在しないものな。

  • なるほど!新たな思考が巡り出す。(Gururiさん、ありがとう!)
  • 以下のように、make_personハンドラ中でPersonオブジェクトを取得する場合、
    • script Personブロックより手前で取得しようとするとエラーになってしまう...。
    • script Personブロックの後ろでは問題なく取得できた。
    • Personをresultに置き換えても問題なく取得できた。
property obj_list : {}

on make_person(aName)
  --set temp to obj_list & Person --構文エラー:属性 Person が何度も指定されています。
  
  script Person
    property firstName : aName
    
    on say_hello()
      display dialog "こんにちは、" & firstName & "です。"
    end say_hello
    
    on set_firstName(aName)
      set firstName to aName
    end set_firstName
  end script
  
  set obj_list to obj_list & Person --resultに置き換えても問題なく取得できた。
  --set obj_list to obj_list & result
end make_person

set sazae to make_person("サザエ")
set tara to make_person("タラ")

say_hello() of item 1 of obj_list
--ダイアログ: "こんにちは、サザエです。"

say_hello() of item 2 of obj_list
--ダイアログ: "こんにちは、タラです。"
  • それでは、make_personハンドラを削除して、トップレベスクリプトの場合はどのような結果になるだろうか?
    • トップレベスクリプトであれば、script Personブロックより手前でも問題なく実行できた。
property obj_list : {}

set Person's firstName to "サザエ"
set obj_list to obj_list & Person

set Person's firstName to "タラ"
set obj_list to obj_list & Person

say_hello() of item 1 of obj_list
--ダイアログ: "こんにちは、タラです。"

say_hello() of item 2 of obj_list
--ダイアログ: "こんにちは、タラです。"

script Person
  property firstName : ""
  
  on say_hello()
    display dialog "こんにちは、" & firstName & "です。"
  end say_hello
  
  on set_firstName(aName)
    set firstName to aName
  end set_firstName
end script
  • さらに、上記スクリプトをそのままに、runハンドラで囲んでみた。
    • AppleScript エラー:Person変数は定義されていません。と警告された。
property obj_list : {}

on run
  set Person's firstName to "サザエ" --AppleScript エラー:Person変数は定義されていません。
  set obj_list to obj_list & Person
  
  set Person's firstName to "タラ"
  set obj_list to obj_list & Person
  
  say_hello() of item 1 of obj_list
  
  say_hello() of item 2 of obj_list
  
  script Person
    property firstName : ""
    
    on say_hello()
      display dialog "こんにちは、" & firstName & "です。"
    end say_hello
    
    on set_firstName(aName)
      set firstName to aName
    end set_firstName
  end script
end run
  • 今度は、setとsay_helloのコードをそっくり、script Personオブジェクトの後ろに移動してみた。
property obj_list : {}

on run
  script Person
    property firstName : ""
    
    on say_hello()
      display dialog "こんにちは、" & firstName & "です。"
    end say_hello
    
    on set_firstName(aName)
      set firstName to aName
    end set_firstName
  end script
  
  set Person's firstName to "サザエ"
  set obj_list to obj_list & Person
  
  set Person's firstName to "タラ"
  set obj_list to obj_list & Person
  
  say_hello() of item 1 of obj_list
  --ダイアログ: "こんにちは、タラです。"
  
  say_hello() of item 2 of obj_list
  --ダイアログ: "こんにちは、タラです。"  
end run
  • 以上の結果から、以下のように解釈することにした。
    • ハンドラ内のコードは、ハンドラが呼び出されて初めて評価(実行)される。
    • script Personブロックも、評価(実行)されて初めてオブジェクトを生成する。
    • 評価されることでPersonオブジェクトが生成され、Person'sやof Person等でアクセスできるようになる。
  • これらのことはrunハンドラ内でも同様であり、よってトップレベスクリプトとrunハンドラ内のスクリプトは、厳密には実行環境が違う。
    • トップレベスクリプト(オブジェクト)は、runメッセージを受け取ったら、とりあえずコード全体を俯瞰して、runハンドラを探す。
    • その際、ハンドラの内側は不要なので評価しない。
    • scriptブロックは評価しておく。だから、スクリプトオブジェクトが生成される。
    • 全体を俯瞰して、runハンドラがないことを確認できたので、最初に戻ってトップレベスクリプトを1行目から順に実行する。
    • もしrunハンドラが見つかれば、runハンドラの内側を1行目から順に実行する。
    • 実行中のコードより後ろにあるscriptブロックは未知なので、そのオブジェクトは生成されていない状態。
    • その状態でオブジェクトにアクセスしようとするとエラーが発生する。
  • 以上のような流れで、実行されていると(自分では)解釈してみた。
  • 最初の疑問に戻って、
    • 初期化ハンドラmake_personではオリジナルのPersonにアクセスできない?のではなく、
    • オリジナルのオブジェクトの生成は、make_personでしか実行できない、ということなのであった。

また少し、AppleScriptの心に近づけた気がした。