promise
Smalltalk
Squeak
SmalltalkDay 15

SqueakでPromises/A+をやってみる


はじめに

今年もやってきましたSmalltalk Advent Calendar。1年ぶりの投稿になります。

年末に引っ越しが控えているため、いつものように自作のライブラリ紹介という訳にはいきません。シンプルな小ネタでいこうと思います。


SqueakがPromises/A+に対応?

普段は仕事でPharoばかり使っているのですが、自称Smalltalkエバンジェリストなので、それ以外のSmalltalk処理系のことも常に気になっています。

10/14日に古豪のOSSのSmalltalk処理系、Squeakが久しぶりにバージョンアップしたとのことで、リリースノートを見ていたのですが、奇妙な記述を見つけました。


"Changed the Promise semantic to be closer to Javascript:"


PromiseをJavaScriptのセマンティクスに近づけた? ちょっと何を言っているかよくわからない...

そこで面白そうなので実際に動かしてみることにしました。

対話環境がSmalltalkの醍醐味です。以後は皆さんもご一緒にどうぞ。


Squeak 5.2 のインストール

https://squeak.org/downloads/

から好きなプラットフォーム用のインストーラを入手します。面倒な人にはAll-in-One(64-bit)がお勧めです。

zipファイルを展開すると起動用スクリプトがあります。Windowsであればsqueak.bat、それ以外はsqueak.shを使います。

モダンなのかレトロなのかよくわからない画面が立ち上がります。

welcome.png

"Configure"を選ぶとテーマの設定など、いろいろできるようですが、年末で忙しいのでここは華麗に"Skip"しましょう。

デフォルトだとフォントが若干小さすぎるため、気になる人はデスクトップメニュー(左クリック)から"appearance..."->"system fonts..."->"demo/hi-dpi mode"で大きくしておくと良いでしょう。

font-menu.png


Promiseを見てみる

真ん中にどーんと表示されている"Welcome to Squeak"のウィンドウですが、実はここからリリースノートなどが読めるようになっています。"Release Notes"->"5.2"を選びスクロールしていくと例のフレーズ"Changed the Promise semantic to be closer to Javascript:"があり、Promiseクラスへのリンクが張られています。

promise-link.png

クリックするとPromiseのブラウザが開きます。

普段Smalltalkerはソース重視なのでコメントはほとんど読みませんが、今回は"instance"、"class"ボタンの隣にある小さな"?"ボタンを押してみましょう。

クラスのコメントが表示されます。

comment2.png


I also implement the Promises/A+ Javascript specification.

うーん本当? 期待して良いのでしょうか。

今度は右クリックメニューで"class refs"を選んで、参照元を見てみましょう。Alt+shift+nがショートカットです。(Macの場合はCommand+shift+n。以後AltキーをCommandキーと読み替えてください)

class-refs.png

25箇所ほどPromiseへの参照が見つかります。PromiseTestというユニットテストのクラスがあるようなので、それを見るのが使い方を把握するには良さそうです。PromiseTestを選択して"browse"をクリックするとブラウザが開きます。

promise-test-browse.png

ユニットテストクラスの場合、ブラウザ上から即座にテストの実行ができます。右クリックメニューから"run all tests"を選ぶと、テストが走り、成功したメソッドがほんのり緑になります。(なんとも地味ですが)

run-all-tests.png

さて、あとはテストメソッド群をつらつらと読んでいけば使い方がわかるでしょう。Promisenewして何かを実行し、成功ならresolveWith:、失敗ならばrejectWith:で結果を返すというのが基本のようです。


どの辺がPromises/A+なのか?

つまりはSqueakに元々あったPromiseに対し、then:ifRejected:を追加し、chainableにつなげて書けるようにした、ということのようです。

こんな風に使います。デスクトップメニューで"open"->"workspace"でワークスペースを開き、以下のコードを貼り付けましょう。


random := 3 atRandom. "1から3までの範囲で乱数生成"
promise := Promise new. "Promise生成"

"3秒後にrandomの値が奇数の時に成功、偶数の時にエラーとなる処理を別スレッドで実行"
[3 seconds wait.
random odd ifTrue: [promise resolveWith: random] ifFalse: [promise rejectWith: (Error new tag: random)] ] fork.

"結果を待つ。成功の場合はOdd: xxxを表示。失敗の時はエラーダイアログを開く"
promise then: [:ans | Transcript cr; show: 'Odd: ', ans] ifRejected: [:err | err signal: 'even'].

"全体の開始を告げる"
Transcript cr; show: 'Start'

実行結果を見るために事前にAlt+tでトランスクリプトを開いておきます。

コードのすべてをAlt+aで選択して、"do it"(Alt+d)しましょう。

まず'Start'と表示されます。別のスレッドで非同期に開始された処理は、3秒後に結果を返してきます。

以下のような感じで表示されるでしょう。

odd.png

運悪く失敗した場合には、エラーダイアログが立ち上がります。

even.png

これで基本的な動きは確認できました。


もう少しJSっぽく拡張してみる

Promises/A+の仕様自体はPromiseの生成方法について何も規定していないので、良いと言えば良いのですが、現状ではnewで生成したpromiseオブジェクトを別スレッドに渡していく必要があり、今ひとつ不便な気がします。

Promisesチェーンを試す前に、JavaScript版でのコンストラクタに該当する部分を、拡張メソッドとして追加することにします。

Promiseをブラウズして、class側を選び、'instance creation'カテゴリを選びます。下のコードペインに次のように書いて、右クリックメニューから"accept"(Alt+s)しましょう。

Promise class >> executor: aBlock メソッド


executor: aBlock
| promise resolver rejecter |
promise := self new.
resolver := [:answer | promise resolveWith: answer].
rejecter := [:error | promise rejectWith: error].
[aBlock value: resolver value: rejecter] fork.
^ promise

中身はなんとなくわかりますね。promise を生成しておいて、成功の時はresolverのブロック経由で正常値を、失敗の時はrejecterのブロック経由でエラーを後から渡せるようにしたというだけのことです。これで明示的にpromiseオブジェクトを別スレッド側に渡す必要がなくなります。生成の処理途中でブロッキングされないようにforkもしています。

accept.png

なお、"accept"時に開発者のイニシャルを聞かれるかもしれません。適当に答えておきましょう。バージョン管理などに使われるのですが、本記事では触れません。

では、まずは簡単なチェーンを書いてみましょう。ワークスペースに以下をコピペし、"do it"してください。


promise := Promise executor: [:resolve :reject |
| rand |
2 seconds wait.
rand := 3 atRandom.
"奇数で成功、偶数の場合はエラーを返す"
rand odd ifTrue: [resolve value: rand ] ifFalse: [reject value: (Error new messageText: 'Even')]
].
((promise then: [:ans | ans * 10])
then: [:ans | ans + 1])
then: [:ans | ans inspect ] ifRejected: [:err | err inspect].

Transcript cr; show: 'Promise chaining start'.

then:が連なっていますね。randが1や3の時は値が引き渡されていき最終結果をinspectできます。

odd-success.png

2の場合はエラーとなるため、最後のifRejected:部分で拾われて、エラーがinspectされるという結果になります。

even-error.png

何度か"do it"してもなかなか失敗しないという方は、ans + 1の箇所をans / 0にしてみても良いでしょう。たとえ奇数の時もZeroDivideのエラーが途中で起こり、ifRejected:で拾われます。

zero-divide.png


Promisesチェーンで運試し

then:を連ねてみましたが、最初以外はすべて同期処理なので気分が出ません。最後に非同期名処理を連ねて'Happy Squeaking Holidays!!'のメッセージを出したいと思います。ワークスペースに以下を貼り付けて"do it"しましょう。


delayedMessenger := [:prevMessage :words |
Promise executor: [:resolve :reject |
1 seconds wait.
nextWord := words atRandom. "候補から適当に選ぶ"
nextMessage := prevMessage asString, ' ', nextWord. "前の文言とつなげる"
nextWord = words last
ifTrue: [reject value: (Error new messageText: nextMessage)] "選ばれたnextWordが最後の要素だった時には失敗"
ifFalse: [resolve value: nextMessage]
].
].

promise := delayedMessenger value: '' value: #('Happy' 'Lucky' 'Wonderful' 'Bad').
((promise then: [:msg | delayedMessenger value: msg value: #('Squeaking' 'Programming' 'Screaming')])
then: [:msg | delayedMessenger value: msg value: #('Holidays' 'Christmas' 'Monday')])
then: [:msg | (msg, '!!') inspect] ifRejected: [:error | (error asString, '!?') inspect].

Transcript cr; show: 'Start'

'Start'の表示後、3秒ほど時間がかかってインスペクタが開きます。

executor:内の処理は非同期で実行されますが、then:で同期してメッセージがうまくつながっていくというわけです。

さて、皆さんは何が表示されたでしょうか? よほど運がいいと以下のように表示されます。

holidays.png

都合の悪いwordが来ると途中でメッセージがつながらなくなり、エラーとして表示されます。

sqreaming.png


終わりに

たまにはSqueakも良いものですね。

ここまで来ればasync/awaitが使えるように拡張していくのも容易ではと思いますが、それはまた次の機会ということで。