文字列をキーにレコードへアクセスするもう一つの方法

  • AppleScriptのレコードは、文字列をキーにアクセスできない。
  • キー値は、ソースコードとして書き込んでおく必要があるのだ。


set a_record to {apple:100, |みかん|:50, |オレンジ|:150} --a_record's "みかん"--NG コンパイルさえできない
a_record's |みかん| --OK

--ちなみに、以下もできない
set a_key to |みかん| --NG "|みかん|変数は定義されていません。"
a_record's a_key

  • そんな殺生な...。これでは、何をやるにも不便過ぎる...。
  • coding()ハンドラを発見して、それを利用した文字列によるレコードへのアクセスは、当初以下のように実装していた。


set a_record to {apple:100, |みかん|:50, |オレンジ|:150} set a_key to "みかん"

set res to for_key1(a_record, a_key) --結果:50



on for_key1(a_record, a_key) run script coding(a_record) & "'s |" & a_key & "|"
end for_key1

on coding(obj) try
if obj's class = text then
"\"" & obj & "\""
else if obj's class = list or obj's class = record then
obj as number
else
obj as text
end if
on error msg
try
"require \"jcode\";$KCODE=\"u\";/\\{.*\\}/=~" & quoted form of msg & ";puts($&);"
do shell script "ruby -e " & quoted form of result
on error
msg's items 1 thru -24 as text
end try
end try
end coding

  • coding()ハンドラでレコードをソースコードに戻して、
  • アクセスしたい文字列キーをソースコードに追加して、
  • run scriptでキー値にアクセスするコードを実行して、取得するのだ。

当初は、これでも簡潔で素晴らしい方法だと思っていたのだが、もっと効率的で素晴らしい方法があった!

もう一つの方法

  • 論より証拠、まずはそのコードを見てみるのだ。
  • 実行すれば、間違いなく「みかん」の50が返る。


set a_record to {apple:100, |みかん|:50, |オレンジ|:150} set a_key to "みかん"

set res to for_key2(a_record, a_key) --結果:50



on for_key2(a_record, a_key) _reader(a_key)'s value_of(a_record) end for_key2

on _reader(k) run script " on value_of(obj) obj's |" & k & "| end me"
end _reader

素晴らしい!

何が起きているのか?
  • _reader()ハンドラにて、a_keyでレコードにアクセスするvalue_of()ハンドラのソースコードを生成して、
  • そのコードの最後をmeとすることで、value_of()ハンドラ自体を含むオブジェクトとして取得できる。
  • 取得したオブジェクトのvalue_of()ハンドラを呼び出すと、それはa_keyでレコードにアクセスするハンドラなので、対応する値が取得できるのだ。
良いところ
  • coding()ハンドラさえ使わずに、たった50文字足らずのテキストをrun scriptするだけで、テキストによるキー値アクセスが実現できているところ。
  • run scriptはコストのかかる処理なので、そこで処理するコードは短ければ短いほど、効率は良くなるのだ。
  • coding()ハンドラを利用する方法では、レコードも含めてすべてをソースコードに展開して、実行していた。
  • もう一つの方法では、レコードはソースコードに展開せず、objという変数で受け渡しするようにしているのだ。

AppleScriptで、こんなことができることに感動!

  • サンプルコードでは、レコードは小さくて単純な作りだが、
  • 実際に利用する時には、もっと巨大で複雑な構造になる。
  • 巨大なレコードのソースをrun scriptするには、相当なコストがかかる。
  • 一方、もう一つの方法なら、いくらレコードが巨大になっても、run scriptのコストは一定なのだ。

さらに短くシンプルに

  • 仕組みは同じだが、_reader()ハンドラに分割せずにコード生成してみた。
  • 依存するハンドラは可能な限り少ない方が良い。もっと言えばない方がいい。
  • この方法ならfor_key()ハンドラの中ですべて完結する。


set a_record to {apple:100, |みかん|:50, |オレンジ|:150} set a_key to "みかん"

set res to for_key3(a_record, a_key) --結果:50



on for_key3(a_record, a_key) run script "
on value_of(obj)
obj's |" & a_key & "|
end
me"
result's value_of(a_record) end for_key3

パフォーマンス計測
  • 1万回ループさせて、処理時間を比較してみた。
  • coding()でレコードのソースに戻す方法だと、明らかに処理に時間がかかっている。
  • もう一つの方法なら、5〜10倍も高速に処理される。


set a_record to {apple:100, |みかん|:50, |オレンジ|:150} set a_key to "みかん"

set t to current date
repeat 10000 times
--set res to for_key1(a_record, a_key) --140, 140, 150
--set res to for_key2(a_record, a_key) --32, 15, 20
set res to for_key3(a_record, a_key) --32, 15, 20
end repeat
(current date) - t
display dialog result
res



on for_key3(a_record, a_key) run script "
on value_of(obj)
obj's |" & a_key & "|
end
me"
result's value_of(a_record) end for_key3

on for_key2(a_record, a_key) _reader(a_key)'s value_of(a_record) end for_key2

on _reader(k) run script "
on value_of(obj)
obj's |" & k & "|
end
me"
end _reader

on for_key1(a_record, a_key) run script coding(a_record) & "'s |" & a_key & "|"
end for_key1

on coding(obj) try
if obj's class = text then
"\"" & obj & "\""
else if obj's class = list or obj's class = record then
obj as number
else
obj as text
end if
on error msg
try
"require \"jcode\";$KCODE=\"u\";/\\{.*\\}/=~" & quoted form of msg & ";puts($&);"
do shell script "ruby -e " & quoted form of result
on error
msg's items 1 thru -24 as text
end try
end try
end coding

キー値アクセス ハンドラの完成

  • 10000回ループなので、1回当りのキー値アクセス平均コストは...
    • 0.0143秒
    • 0.0022秒
  • どちらも極めて短い時間になるので、どうでも良いように感じるが、
  • キー値アクセスのような基本的な機能は、ループの中で大量に繰り返される可能性がある。
  • できる限り効率良く処理されるようにしておくべきなのだ。
  • また、基本機能を提供するハンドラについては、できる限り依存するハンドラは少なく、できれば依存コードなしにしたい。

以上のことを考慮して、for_key()、set_key_value()ハンドラは以下のようになった!


set a_record to {apple:100, |みかん|:50, |オレンジ|:150}
for_key(a_record, "みかん") --結果:50


set a_record to set_key_value(a_record, "みかん", 500) a_record --結果:{apple:100, |みかん|:500, |オレンジ|:150}


set a_record to set_key_value(a_record, "バナナ", 80) a_record --結果:{apple:100, |みかん|:500, |オレンジ|:150, |バナナ|:80}



on for_key(a_record, a_key) run script "
on value_of(obj)
obj's |" & a_key & "|
end
me"
result's value_of(a_record) end for_key

on set_key_value(a_record, a_key, a_value) run script "
on set_value(obj, v)
try
set obj's |" & a_key & "| to v
obj
on error number -10006
obj & {|" & a_key & "|:v}
end try
end
me"
result's set_value(a_record, a_value) end set_key_value

参考ページ

以上の仕組みは、すべてScript factory >> XModules >> XAccessorから頂きました。感謝です!

  • Script factoryにはAppleScriptにまつわる、高度な技の話しが盛りだくさんで、読み始めると止まらなくなる。
  • 以前から知っていたが、当時は自分の知識と経験が不足していて、その話しの価値に気付けなかった...。

今なら分かる!


AppleScriptでこのような処理ができることを知って、またしても新たな世界が開けてしまった!

  • この可能性を知ると、試してみたいこと盛りだくさんで、最近ちょっと寝る間が惜しい...。