Smalltalkのtは小文字です

id:sumim:about

オブジェクト指向の“モヤッと”の正体を知りたくなったら、id:sumim:20080415:p1id:sumim:20040525:p1 がお役に立つかも…。


 

2016-06-25

[] 現在の Smalltalk(すなわち、-80以降)と Smalltalk-76, -72における true, false の扱いの違いを調べてみた



元の話の発端が何かは分からなかったのですが、最近 Ruby の true, false の属するクラスについての言及


や、関連する過去のこんな記事


を見かけたので、今の Smalltalk と、その元になっている Smalltalk-80 より前に作られた Smalltalk-76、Smalltalk-72 ではどんなふうになっていたか調べてまとめてみました。



▼ 現在の Smalltalk における true, false (Smalltalk-80 以降)

先の言及にもあるように Smalltalk-80 を元にしている今の Smalltalk(Pharo、SqueakVisualWorks などの直系の子孫。GNU Smalltalk などのファンお手製の変わり種実装を含む)では、true は Trueクラス、false は Falseクラスの唯一のインスタンスで、さらに Trueクラス、Falseクラスは共通の Booleanクラスのサブクラスになっています。

true class. "=> True "
false class. "=> False "
True superclass. "=> Boolean "
False superclass. "=> Boolean "

nil class. "=> UndefinedObject "
UndefinedObject superclass. "=> Object "

Ruby と異なり、現在の Smalltalk では、if 式は true や false へのメッセージ送信として記述します。たとえば、次の式の場合、3 < 4 (これも 3 への < 4 というメッセージ送信)の結果の true に対して、ifTrue: [5] ifFalse: [6] というメッセージが送信されます。

3 < 4 ifTrue: [5] ifFalse: [6] "=> 5 "

通常のメッセージ式と同じように解釈されるならば、これは true の属する Trueクラスに定義された ifTrue:ifFalse: というメソッドを [5]、[6] という引数を伴ったコールとして機能します(実際にはコンパイル時にインライン展開されていわゆる GOTO を使ったコードに置き換えられるので、通常のコードでは ifTrue:ifFalse: などのメソッド本体がコールされることはありません。念のため)。

分かりやすく Ruby 風に(メソッド名に使えないコロンをアンダーバーに置き換えて)書き下すとこんなかんじになりますか。

(3 < 4).ifTrue_ifFalse_(->{5}, ->{6})

普段使いの Squeak4.3J で調べると、True(もしくは False)にはこんなメソッド群が定義されています。

True selectors.
"=> #(#and: #| #ifTrue:ifFalse: #not #ifFalse:ifTrue: #==> #ifTrue: #or: #ifFalse: #printOn: 
#xor: #& #asBit) "

そのスーパークラスである Boolean に定義されているメソッド群はこんなふうになります。

Boolean selectors.
"=> #(#ifTrue:ifFalse: #or:or: #and:and:and: #not #or:or:or: #and:and: #or:or:or:or:or: #& 
#veryDeepCopyWith: #and:and:and:and: #eqv: #| #or:or:or:or: #storeOn: #deepCopy #basicType 
#clone #isLiteral #ifFalse:ifTrue: #shallowCopy #==> #ifTrue: #or: #ifFalse: 
#newTileMorphRepresentative #and:) "

このうち、サブクラスの True, False が再定義している #(#ifTrue:ifFalse: #not #& #| #ifFalse:ifTrue: #==> #ifTrue: #or: #ifFalse: #and:) の中身はすべて self subclassResponsibility と記述されています。これらのメソッドはコールされることはないですし、コールされても例外があがるだけで意味をなさず定義は無用なのですが、きっと抽象データ型OOPの部分的サポートを意識した設計になっているのでしょうね。



▼Smalltalk-76 における true, false

Smalltalk は仕様のみで実装まで至らなかった Smalltalk-71 を除いて、大きく分けて 1970年代に Smalltalk-72、Smalltalk-76 という「二つの言語」が作られました(実際にはさらに -72 の高速版の -74、-76 のシュリンク版の -78 も作られています)。この「二つの言語」という言い回しは、通常の言語からすると妙に聞こえると思います。

プログラミング言語は、バージョンが上がるごとに機能が増えたり文法が拡張・変更されたりしつつも、言語としては同じ物と認識されるのが普通です。しかし Smalltalk の場合は違っていて、メッセージングによるプログラミングというコンセプトを共有することを除けば、Smalltalk-72 と -76 は言語としてはまったく別物なので注意を要します。

Smalltalk-80 とは比較的時代の近い Smalltalk-76 は、Smalltalk で見慣れたメッセージ式文法やカラムUI を採用したクラスブラウザ等の IDE関連 GUIツールの存在など Smalltalk-80 と似た部分も多いのですが、Smalltalk-80 でよく知られているメタクラスや、後にクロージャーで実装される第一級の無名関数オブジェクトが無かったり、いわゆる 〜ect 系のコレクションメソッド群を持たないなど、今の Smalltalk の特徴とされる機能を多く欠いていて興味深いです。

一方で、Smalltalk-76 には通常の言語にある if式構文が用意されています。つまり Smalltalk-80 のように true, false へのメッセージ送信では表現しないのです。これは Smalltalk に慣れ親しんだ者としてはちょっとした衝撃の事実ですね。たとえば先に書いた現在の Smalltalk の 3 < 4 ifTrue: [5] ifFalse: [6] という処理は、Smalltalk-76 では次のように記述します。

http://squab.no-ip.com/collab/uploads/st76ifthenelse.png


同様に、for, while, until といったループの式構文も用意されています。

http://squab.no-ip.com/collab/uploads/st76forwhileuntil.png


こんな Smalltalk-76 ですが、本題の true, false はどのような扱いになっているのでしょうか。それぞれが属するクラスを調べてみます。処理系は 2014年にリバイブされた Smalltalk-78 を使用しました(先のスクリーンショットも同様)。

true class "⇒ Class Object "
true hash "⇒ 2 "

false class "⇒ Class Object "
false hash "⇒ 1 "

nil class "⇒ Class Object "
nil hash "⇒ 0 "

Object new class "⇒ Class Object "
Object new hash "⇒ 4867 "

Class Object というのは単に Objectクラスのことのようです。実に面白い。なんと Smalltalk-76 では true, false そして nil は専用のクラスが用意されておらず、Object の普通のインスタンスなのです。hash が 2, 1, 0 なのも象徴的です。



▼Smalltalk-72 における true, false

Smalltalk-72 は先にも述べたとおり現在の Smalltalk はおろか、Smalltalk-76 ともまったくの別言語です。クラスは JavaScript のようにコンストラクタを兼ねた関数で、その中にメソッドがパターンマッチを用いた文法解析処理のように記述されます。クラスの継承機構はありません。したがって self subclassResponsibility に象徴されるような、現在主流の抽象データ型のOOP の汚染を受けていないので、アラン・ケイのメッセージングOOP の心を学ぶには、ぜったい外せない処理系とも言えます。

参考まで、簡単な Joe the box デモ(下のツイートを参照)程度であれば Smalltalk-78 同様、Lively-Web にリバイブされた ALTO/Smalltalk-72 エミュレーターが手軽に使えます。

さらに組み込みのエディタなども活用して Smalltalk-72 を本格的に体験したいということであれば、Squeak Smalltalk に関する知識がそれなりに必要にはなりますが Squeak3.2 で動く Smalltalk-72 エミュレーターがお薦めです。今回は後者を使います。


さて、Smalltalk-76 が Object のインスタンスで true, false, nil を表わしていたのだから、それ以前の Smalltalk-72 でも同じだろう…と安直に考えていたのですが、調べてみるとどうやら違うようです。そもそも Smalltak-72 では Smalltalk-76 と違って、true, false へのメッセージ送信による条件分岐処理に戻って(?)いますので、この時点でもう間違っています。^^;

Smalltalk-72 では「 真偽値を返す式 ⇒ (真の時に評価する式) 偽の時に評価する式 」で条件分岐を記述します。(この ⇒ は ? で、後の is? で使う ? は ~ で入力できます。! は Lively-Web版では \ 、Squeak3.2版では上方向カーソルキーです。)

http://squab.no-ip.com/collab/uploads/st72ifthenelse.png


ちなみに Smalltalk-72 では、メソッドの定義はパターンマッチで記述された文法の定義のようなものなので、if オブジェクトに対するメッセージ送信の形で ALGOL系の if-then-else も定義可能で、実際にそうした記法を可能にするコードもエミュレーターには含まれています。

http://squab.no-ip.com/collab/uploads/st72algoltypeif.png


これを踏まえて true, false ついでに nil がどのように扱いかを調べてみましょう。Smalltalk-72 ではオブジェクトに自身が属するクラスを訊ねるためには is? というメッセージを送信します。

http://squab.no-ip.com/collab/uploads/st72truefalsenil.png


true は atom と称したシンボルオブジェクトに属します。一方、false は is? メッセージに対して自身を返している、あるいは false がクラスであるかのように振る舞いますが、実際には false は falseclass のインスタンスです。

show falseclass で定義を確認すると、どうやら is や is? メッセージを受け取った際に自身を返すように、さらにご丁寧に本来 偽(false) であるはずの false is false が 真(true) を返すようなパターンマッチを用意してまで特殊な振る舞いがコードされているようです。

http://squab.no-ip.com/collab/uploads/st72showfalseclass.png


false 向けのいくつかのメソッドはプリミティブ(CODE 11)として記述されておりユーザーからは見えないのですが、is やそれに続いて ? がメッセージと送られた場合にどういう振る舞いになるかは Smalltalk-72 自身で記述されており、false is? が falseclass ではなく false を返していることがこの定義を見ると分かります。

この特殊な振る舞いの定義を削ってしまえば正しく応答してくれるはずなのですが、ただ is メソッドはクラスごとに実装する必要があるようで、これを欠いてしまうと is? メッセージに対して正しい反応ができないようです。

http://squab.no-ip.com/collab/uploads/st72addtois.png


そこで、falseclass の特殊な振る舞いの is の定義をいったん削除して他の通常のクラスのような is メソッドを定義してみました。(残念ながら、組み込みのエディタを用いたこの操作は、なぜかマウスクリックをエミュレーターが検出しない Lively-Web 版ではできません。あしからず。)

http://squab.no-ip.com/collab/uploads/st72editfalseclass01.png

http://squab.no-ip.com/collab/uploads/st72editfalseclass02.png

http://squab.no-ip.com/collab/uploads/st72editfalseclass03.png

http://squab.no-ip.com/collab/uploads/st72editfalseclass04.png


これで false も is? メッセージに嘘をつかなくなりましたので再び試してみます。

http://squab.no-ip.com/collab/uploads/st72editfalseclass05.png



▼まとめ

Smalltalk の状況に限れば、察するに true, false にメッセージを送って条件分岐をするなど必要がなければ、それぞれにクラスは無くても大丈夫で(Smalltalk-76)、もしその必要があっても、false 以外は真扱いにするのであれば、false クラスだけで用は足りる(Smalltalk-72)ということになりそうです。

さらに、true のみに真の振る舞いをさせるなら True クラスも必要で、加えて真偽値の振る舞い(メソッド)を増やしたり、その際にテンプレートメソッドパターンを活用したいとき、あるいは抽象データ型OOPを限定的にでもサポートすることを考えた場合は Boolean クラスも用意しておくのが便利(Smalltalk-80)なようです。

2016-03-15

[] 平和な動物園を作ろう!をインスタンス特異的メソッドを用いてSqueak Smalltalk


http://echo.2ch.net/test/read.cgi/tech/1444216746/361 経由で、

あなたは,さいたま動物園の園長に選ばれました.さいたま動物園には全部で10種類の動物たちがいます.あなたの園長としての初仕事は,これらの動物たちをどのオリに入れるかを決めることになりました.

さて,ここで問題なのは,

・動物たちには相性の良し悪しがある.

・相性の悪い動物たちをお互いに近いオリにいれると,みんなが暴れだしてしまう.

・動物たちの不満度が小さくなるようにオリを選んでやる必要がある.

ということです.


動物たちの不満度は,

 (各オリの間の距離) × (各動物の間の相性)の総和

で表されます.


さぁ,地図に示されたオリに動物たちをうまく割り当てて,動物たちの不満度が小さい平和な動物園を作ってください.

平和な動物園を作ろう! ―2次割当て問題って何?― 埼玉大学工学部情報システム工学科池口研究室

手抜きをすべく、Matrix で行あるいは列単位で permutationsDo: を使いたかったのですが、そもそも Matrix は SequenceableCollection のサブクラスではなかったので permutationsDo: は端から使えないことが発覚( permutationsDo: は SequenceableCollection に定義されている。為念)。そこで、配列の配列を使うことにしました。

ただし素朴にデータだけからなる配列の配列では、並べ替えた際に動物との対応が面倒になるので、key に動物名、value に配列を持たせた Association を要素にしました。

animals := {
   'ライオン' -> #(0 2 6 4 6 2 4 4 2 4).
   'ワニ' -> #(2 0 4 2 2 2 2 2 2 6).
   'ニシキヘビ' -> #(6 4 0 2 6 8 8 6 4 8).
   'オオカミ' -> #(4 2 2 0 4 2 6 6 2 6).
   'トラ' -> #(6 2 6 4 0 2 4 4 2 4).
   'スイギュウ' -> #(2 2 8 2 2 0 6 6 6 8).
   'サイ' -> #(4 2 8 6 4 6 0 6 6 4).
   'カバ' -> #(4 2 8 6 4 6 6 0 6 6).
   'インパラ' -> #(2 2 4 2 2 6 6 6 0 6).
   'ゾウ' -> #(4 6 8 6 4 8 4 6 6 0)}.

こうしておけば動物名も一緒にスワップできるので何かと便利で一件落着…かと思いきや、動物をスワップしたら、その動物との相性を記したデータの対応する位置の要素も連動してスワップさせないといけません。

うーむ、やはり permutationsDo: 相当を書くしかないのかな…と諦めかけたのですが、それだとなんか負けた気(謎)がします。


あらためて SequenceableCollection>>#permutationsDo: 内の処理を眺めてみると、size と swap:with: しか使われていないことが分かります。

SequenceableCollection >> permutationsDo: aBlock
"Repeatly value aBlock with a single copy of the receiver. Reorder the copy
so that aBlock is presented all (self size factorial) possible permutations."

"(1 to: 4) permutationsDo: [:each | Transcript cr; show: each printString]"

self shallowCopy permutationsStartingAt: 1 do: aBlock

SequenceableCollection >> permutationsStartingAt: anInteger do: aBlock
"#(1 2 3 4) permutationsDo: [:each | Transcript cr; show: each printString]"

anInteger > self size ifTrue: [^self].
anInteger = self size ifTrue: [^aBlock value: self].
anInteger to: self size do:
[:i | self swap: anInteger with: i.
self permutationsStartingAt: anInteger + 1 do: aBlock.
self swap: anInteger with: i]


つまり、animals に対して swap:with: で前述の処理(行・列要素のスワップの連動)を行なうようなんとか多態させることさえできれば、permutationsDo: を使って手を抜くという目的は果たせそうです。

とはいえ animals を permutationsDo: するためだけに swap:with: を書き換えてしまうのは、何か違う気がするので(というか、ダメ。ゼッタイ。w)、assureUniClass してインスタンス特異的クラスを作成し、インスタンス特異的メソッドとして swap:with: を再定義することにしました。


…というような腑抜けた方針で書いたのが、このコードです。



| animals cages ans |

animals := {
   'ライオン' -> #(0 2 6 4 6 2 4 4 2 4).
   'ワニ' -> #(2 0 4 2 2 2 2 2 2 6).
   'ニシキヘビ' -> #(6 4 0 2 6 8 8 6 4 8).
   'オオカミ' -> #(4 2 2 0 4 2 6 6 2 6).
   'トラ' -> #(6 2 6 4 0 2 4 4 2 4).
   'スイギュウ' -> #(2 2 8 2 2 0 6 6 6 8).
   'サイ' -> #(4 2 8 6 4 6 0 6 6 4).
   'カバ' -> #(4 2 8 6 4 6 6 0 6 6).
   'インパラ' -> #(2 2 4 2 2 6 6 6 0 6).
   'ゾウ' -> #(4 6 8 6 4 8 4 6 6 0)}.

cages := #(
   (0 3 4 5 8 10 9 6 2 4)
   (3 0 4 4 7 9 9 8 5 9)
   (4 4 0 2 4 7 5 4 4 8)
   (5 4 2 0 3 5 5 5 5 9)
   (8 7 4 3 0 3 5 6 8 12)
   (10 9 7 5 3 0 4 7 10 14)
   (9 9 5 5 5 4 0 3 8 11)
   (6 8 4 5 6 7 3 0 5 8)
   (2 5 4 5 8 10 8 5 0 4)
   (4 9 8 9 12 14 11 8 4 0)).

ans := Set new -> Float infinity.
animals assureUniClass class compile: 'swap: i with: j
   super swap: i with: j.
   self do: [:each | each value swap: i with: j]'.
animals permutationsDo: [:perm |
   | keys values sum |
   keys := perm collect: #key. "keys asString displayAt: 20@20."
   values := perm collect: #value.
   sum := (values * cages) sum sum.
   ans value = sum ifTrue: [ans key add: keys].
   ans value > sum ifTrue: [ans := (Set with: keys) -> sum]].
^ans

"=> a Set(
   an Array1('スイギュウ' 'インパラ' 'ニシキヘビ' 'カバ' 'サイ' 'オオカミ' 'トラ' 'ライオン' 'ゾウ' 'ワニ')
   an Array1('スイギュウ' 'インパラ' 'ニシキヘビ' 'カバ' 'サイ' 'オオカミ' 'ライオン' 'トラ' 'ゾウ' 'ワニ')
)->2160


その後よく考えたら、素直に書いた方がシンプルだし速かったでござるの巻。あと、パラメーターのコピペミスがあったので、結果と共に差し替えました。orz

| animals cages ans |

animals := #(
   (0 2 6 4 6 2 4 4 2 4)
   (2 0 4 2 2 2 2 2 2 6)
   (6 4 0 2 6 8 8 6 4 8)
   (4 2 2 0 4 2 6 6 2 6)
   (6 2 6 4 0 2 4 4 2 4)
   (2 2 8 2 2 0 6 6 6 8)
   (4 2 8 6 4 6 0 6 6 4)
   (4 2 8 6 4 6 6 0 6 6)
   (2 2 4 2 2 6 6 6 0 6)
   (4 6 8 6 4 8 4 6 6 0)).

cages := #(
   (0 3 4 5 8 10 9 6 2 4)
   (3 0 4 4 7 9 9 8 5 9)
   (4 4 0 2 4 7 5 4 4 8)
   (5 4 2 0 3 5 5 5 5 9)
   (8 7 4 3 0 3 5 6 8 12)
   (10 9 7 5 3 0 4 7 10 14)
   (9 9 5 5 5 4 0 3 8 11)
   (6 8 4 5 6 7 3 0 5 8)
   (2 5 4 5 8 10 8 5 0 4)
   (4 9 8 9 12 14 11 8 4 0)).

ans := Set new -> Float infinity.
(1 to: animals size) permutationsDo: [:perm |
   | sum |
   sum := 0.
   perm doWithIndex: [:pi :i |
      perm doWithIndex: [:pj :j |
         sum := ((animals at: pi) at: pj) * ((cages at: i) at: j) + sum]].
   ans value = sum ifTrue: [ans key add: perm copy].
   ans value > sum ifTrue: [ans := (Set with: perm copy) -> sum]].
ans

"=> a Set(#(6 9 3 8 7 4 5 1 10 2) #(6 9 3 8 7 4 1 5 10 2))->2160 "


さらに追記。

なんと出題の動物の相性のデータにも対称になっていないという誤りがあったみたいで、

| animals |
animals := #(
   (0 2 6 4 6 2 4 4 2 4)
   (2 0 4 2 2 2 2 2 2 6)
   (6 4 0 2 6 8 8 6 4 8)
   (4 2 2 0 4 2 6 6 2 6)
   (6 2 6 4 0 2 4 4 2 4)
   (2 2 8 2 2 0 6 6 6 8)
   (4 2 8 6 4 6 0 6 6 4)
   (4 2 8 6 4 6 6 0 6 6)
   (2 2 4 2 2 6 6 6 0 6)
   (4 6 8 6 4 8 4 6 6 0)).

animals - ((1 to: animals size) collect: [:idx | animals collect: [:each | each at: idx]])
=> #(
   (0 0 0 0 0 0 0 0 0 0)
   (0 0 0 0 0 0 0 0 0 0)
   (0 0 0 0 0 0 0 -2 0 0)
   (0 0 0 0 0 0 0 0 0 0)
   (0 0 0 0 0 0 0 0 0 0)
   (0 0 0 0 0 0 0 0 0 0)
   (0 0 0 0 0 0 0 0 0 0)
   (0 0 2 0 0 0 0 0 0 0)
   (0 0 0 0 0 0 0 0 0 0)
   (0 0 0 0 0 0 0 0 0 0))

対称となっていない相性の値がそれぞれ 8 の場合と 6 の場合で計算し直すと、結果は次のようになりました。

8 => a Set(
   #(6 9 3 8 7 4 5 1 10 2)
   #(6 9 3 8 7 4 1 5 10 2)
)->2164
6 => a Set(
   #(6 9 3 7 1 5 4 8 10 2)
   #(6 9 3 7 5 1 4 8 10 2)
   #(6 9 3 8 7 4 1 5 10 2)
   #(6 9 3 8 7 4 5 1 10 2)
   #(6 9 7 8 4 1 5 3 10 2)
   #(6 9 7 8 4 5 1 3 10 2)
   #(6 9 8 7 4 1 5 3 10 2)
   #(6 9 8 7 4 5 1 3 10 2)
)->2156

[]「『出現確率1%のガチャを100回引いても,4割近くの人は全部はずれる。“本当の確率”を読み解いてみよう』を素直に解いてみる」をSqueak Smalltalk




「1 - 100回引いてハズす確率」ではなく、各回の当たる確率を積算して算出する計算はどうなるか、というお話にからめて、最後に添えられた Ruby 版で、Smalltalk でもおなじみの inject (Smalltalk では inject:into: )が使われていたので Squeak Smalltalk でも書いてみました。

(0 to: 99) inject: 0 into: [:r :n | r+((0.99 raisedTo: n)*0.01)] "=> 0.63396765872677 "

残念ながら Squeak には raisedTo: のエイリアスとして ** が用意されてないのと(Pharo にはあるらしい)、二項メッセージ式に優先順位がないせいで括弧が増えるのがアレですが、よく似ていますね。


なお、Smalltalk でも Squeak や Pharo に限れば、APL 譲り(…とういかワナビ?)の配列計算が使えるので、ちょっと趣を変えて同じようなことをこんなふうにも書くことができます。

((0.99 raisedTo: (0 to: 99)) * 0.01) sum "=> 0.6339676587267705 "

Squeak や Pharo の raisedTo: は、引数に配列を与えれば答えを配列で返すしくみになっています( raisedTo: の返値になぜか生じる丸め誤差が見苦しいので rounded しています)。

(3 raisedTo: (0 to: 4)) rounded "=> #(1 3 9 27 81) "

ではメッセージ raisedTo: 〜 のレシーバーが配列なら、配列の配列が返るかというとそうはならず、対応した各要素について累乗値が返ってきます。

#(3 4 5) raisedTo: #(0 1 2) "=> #(1 4 25) "

したがって、レシーバーと raisedTo: の引数の配列のサイズが違うとエラーになるので要注意です。

#(3 4 5 6) raisedTo: #(0 1 2) "=> Error: otherCollection must be the same size "

余談ですが、整数の累乗なのに配列だと Float に変換されてしまう謎も含め、なぜこのような振る舞いになるかというのは、Number>>#raisedTo: の定義をみると分かります。

Number >> raisedTo: aNumber 
"Answer the receiver raised to aNumber."

aNumber isInteger ifTrue: [
"Do the special case of integer power"
^ self raisedToInteger: aNumber].
aNumber isFraction ifTrue: [
"Special case for fraction power"
^ (self nthRoot: aNumber denominator) raisedToInteger: aNumber numerator ].
self < 0 ifTrue: [
^ ArithmeticError signal: 'Negative numbers can''t be raised to float powers.' ].
0 = aNumber ifTrue: [^ self class one]. "Special case of exponent=0"
1 = aNumber ifTrue: [^ self]. "Special case of exponent=1"
0 = self ifTrue: [ "Special case of self = 0"
aNumber < 0
ifTrue: [^ (ZeroDivide dividend: self) signal]
ifFalse: [^ self]].
^ (aNumber * self ln) exp "Otherwise use logarithms"


なお、レシーバーが配列の場合は、まず Collection>>#raisedTo: が呼ばれるので、レシーバーが整数の場合とは振る舞いが異なってきます。

Collection >> raisedTo: arg
^ arg adaptToCollection: self andSend: #raisedTo:

Collection >> adaptToCollection: rcvr andSend: selector
"If I am involved in arithmetic with another Collection, return a Collection of
the results of each element combined with the scalar in that expression."


rcvr isSequenceable & self isSequenceable ifFalse:
[self error: 'Only sequenceable collections may be combined arithmetically'].
^ rcvr with: self collect:
[:rcvrElement :myElement | rcvrElement perform: selector with: myElement]

2016-02-25

[] Smalltalk-76(-78)のArrayのAPIが思ったよりSmalltalk-80と違っていて面白かったので、気付いた範囲でいくつか拾ってみた




| x .
"at:put:, yourself"    ↪(1 2 3) ◦ 2 ← 100; itself "⇒ (1 100 3 ) ".
"atLast:put:"          ↪(1 2 3) last ← 100; itself "⇒ (1 2 100 ) ".
"replaceFrom:to:with:" ↪(1 2 3 4 5 6) ◦ (2 to: 3) ← ↪(20 30); itself "⇒ (1 20 30 4 5 6 ) ".
"size"                 (1 to: 10) length "⇒ 10 ".
"includes:"            (1 to: 10) has: 5 "⇒ true ".
"collect:"             (1 to: 10) transform▹ x to▹ [x * 2] "⇒ (2 4 6 8 10 12 14 16 18 20 ) ".
"select:"              (1 to: 10) all▹ x suchThat▹ [x \ 2 ≡ 0] "⇒ (2 4 6 8 10 ) ".
"detect:"              (1 to: 10) find▹ x suchThat▹ [x > 3] "⇒ 4 ".
"inject: 0 into: #+"   (1 to: 10) sumTo: 100 "⇒ 155 ".
"asBag sortedCounts"   ↪(1 1 2 3 3 3 4 5 5) frequencies "⇒ ((1 4 ) (1 2 ) (2 1 ) (2 5 ) (3 3 ) ) ".

http://squab.no-ip.com/collab/uploads/st80-vs-st76.png

“▹”は白抜きの“:”(オープンコロン。件のエミュレーターでは : を二回タイプして入力できる。ちなみに“◦”は . を二回、↪ は # )。上のテキストを Smalltalk-78 のワークスペース(デスクトップ右クリック → open workspace )などにペーストして使用可能です( ctrl + v 。ホストOS とブラウザの組み合わせによって機能しないことがあります。当方は Win8.1 + Chrome を使用)。変数 x を使用する式は冒頭の | x. から評価してみたい行の最後のピリオドの手前まで(つまり、最後のピリオドを選択範囲に含めない)、変数 x は特に使用しない式なら行の頭からやはりピリオドの手前まで選択して右クリック → doit すると実行、結果の表示ができます。

Ruby にも同一メソッドの別名の一方として採用されている #collect: 、#select: 、#detect: 、#inject:into: などのいわゆる 〜ect系は Smalltalk-76(-78) にはまだなくて、比較的のちの Smalltalk-80 になって取り入れられたものなのですね。


こういう面白い機能も見つけました。UserView の同名メソッドを介して、ウインドウのアクティベート等に使われたようです。のちの MVC の ControlManager では OrderedCollection で remove: してから addFirst: するイデオムに置き換えられていました。

↪(1 2 3 4 5) promote: 3 "⇒ (3 1 2 4 5 ) ".


Smalltalk-72、-76、-80 という三世代の Smalltalk の文法の変化も面白いですが、比較的よく似ている -76(-78) から -80 への API の変化を追いかけるのも楽しそうですね。



▼おまけ

Smalltalk-72

https://pbs.twimg.com/media/Cb5th-0UYAAosCT.png


Smalltalk-76(-78)

https://pbs.twimg.com/media/Cb5tiCWUcAI9jVp.png


Smalltalk-80

https://pbs.twimg.com/media/CcBaiNkUcAAwF-f.png

2015-12-01

[] Squeak5.0で日本語表示をするシリーズ: とりあえずJapaneseEnvironmentを直してStrikeFontで日本語を表示できるようにする



おそらくここらへんは将来的にはなくなる予定の仕組みなのかもしれないのですが、とりあえず淡々と直していきましょう。いったん TrueType のことは忘れて StrikeFont(ビットマップフォント)で日本語を表示できるようにします。


まず最初に、既定のフォントをインストールします。(要ネット接続)

(Locale isoLanguage: 'ja') languageEnvironment installFont

うまくインストールできれば、表示フォントに Accuny あるいは DefaultMultiFont を指定した状態で次のコードを print it すると、「あ」が表示されるはずです。

Character leadingChar: JapaneseEnvironment leadingChar code: 12354 

http://squab.no-ip.com/collab/uploads/Sq5JapaneseEnvFix01.png


念のため、Accunyフォントの fontArray を調べるとサイズが 6(JapaneseEnvironment leadingChar + 1)に増えて、その 6 番目めに日本語フォントが新しく入っているのが確認できます。

(TextStyle named: 'Accuny') defaultFont fontArray
"=> {a StrikeFont(Accuny11 12) . nil . nil . nil . nil . a StrikeFont(Japanese10 12)} "

ここで、なぜか無くなっている StrikeFontSet>>#copy を追加しておきます。

| urlStr |
urlStr := 'http://squeak-ja.sourceforge.jp/patches/PatchesJa20111005-4.2.sar'.
SARInstaller new fileInFrom: urlStr asUrl retrieveContents contents asByteArray readStream

以下のスクリプトでデフォルトのフォントを日本語表示可能な Accuny に変えることができます。

| style normalFont windows |
style := TextStyle named: #DefaultMultiStyle.
style defaultFontIndex: 3.
normalFont := style defaultFont.
smallFont := style fontAt: style defaultFontIndex - 2.
Preferences class selectors
   select: [:sel | (sel beginsWith: 'set') and: [sel endsWith: 'FontTo:']]
   thenDo: [:sel | Preferences perform: sel with: normalFont].
Preferences setPaintBoxButtonFontTo: smallFont.
Preferences setBalloonHelpFontTo: smallFont.
BalloonMorph setBalloonFontTo: smallFont.
windows := SystemWindow
   windowsIn: ActiveWorld
   satisfying: [:sw | sw model isKindOf: Workspace].
windows do: [:ws |
   (ws findA: PluggableTextMorph) ifNotNil: [:morph |  morph font: normalFont]]


次に Locale を日本語に変更して、IME からの入力やコピー&ペースト、ファイルの入出力に適切なエンコーディングが使われるようにするしくみ使えるようにします。

Locale currentPlatform: (Locale isoLanguage: 'ja')

しかし、これをするとととたんにいろいろとおかしなことが起こり始めるので、わかる範囲で直していきましょう。コピー&ペーストなどもできなくなることがあるので、適宜 Locale currentPlatform: (Locale isoLanguage: 'en') で元にもどしながら操作するのがよいと思います。なお、以下は Windows 環境での作業ですので、他の OS 向けには適宜読み替えて(あるいは試行錯誤してみて)ください。



▼ LanguageEnvironment class>>#isAlphaNumeric: の追加

この状態で IME から「あ」などと入力してみて最初に気づくのは MessageNotUnderstood: JapaneseEnvironment class>>isAlphaNumeric: エラーです。

http://squab.no-ip.com/collab/uploads/Sq5JapaneseEnvFix02.png


とりあえず、JapaneseEnvironment class のスーパークラスの LangageEnvironment class に、すでにある #is〜: メソッド群を参考に self charsetClass に委譲する #isAlphaNumeric: メソッドを新しく追加します。

どのように定義してもよいのですが、ここでは次のようにします。まず、 #isDigit: を implementors of it (alt/cmd + m) するなどして定義を呼び出します。

http://squab.no-ip.com/collab/uploads/Sq5JapaneseEnvFix03.png


ここで最初の Digit を選択して AlphaNumeric をタイプするなどして置き換え、続けて alt/cmd + j (again) して次のも同様に置き換えます。

http://squab.no-ip.com/collab/uploads/Sq5JapaneseEnvFix04.png

http://squab.no-ip.com/collab/uploads/Sq5JapaneseEnvFix05.png


置き換えたら accept (alt/cmd + s) します。すると、#isAlphaNumeric: の定義はいったん消えて元の #isDigit: の定義に戻ってしまいますが、これは当該ウインドウが #isDigit: の implementors of it で開かれたからで、LanguageEnvironment を brorse it (alt/cmd + b) してブラウザをクラスメソッド一覧に切り替えるか、isAlphaNumric: についてあらためて implementors of it (alt/cmd + m) してやれば、#isAlphaNumric: がきちんと定義されているのが確認できるはずです。

これで、もしノーティファイアがそのままなら Proceed して消せますし、以後、IME 経由での日本語の入力時に同様のノーティファイアが現われることもないはずです。



▼スクロールホイールの操作が反応しなくなっているのを直す

手元の Win環境では、Locale を ja に切り替えると、ブラウザなどでスクロールをしようとしてスクロールホイールを回しても、ペインの中がフラッシュするだけでうまく動作しないようです。これは JapaneseEnvrironment class>>#inputInterpreterClass で Win 向けなどに指定されている UTF32JPInputInterpreter で、スクロールホイールのイベントうまく取得できていないのが原因のようです。そこで、次の赤字の部分を付け足して accept (alt/cmd + s) します。

UTF32JPInputInterpreter >> nextCharFrom: sensor firstEvt: evtBuf
    | keyValue mark |
    keyValue := evtBuf at: 6.
    keyValue = 0
        ifTrue: [keyValue := evtBuf at: 3].
    mark := self japaneseSpecialMark: keyValue.
    mark notNil
        ifTrue: [^ mark].
    keyValue < 256
        ifTrue: [^ (Character value: keyValue) squeakToIso].
    ^ Character leadingChar: JapaneseEnvironment leadingChar code: keyValue



▼コピー&ペーストがうまくいかないのを直す

JapaneseEnvironment >> clipboardInterpreterClass
    | platformName osVersion |
    platformName := Smalltalk platformName.
    osVersion := Smalltalk osVersion.
    (platformName = 'Win32'
            and: [osVersion = 'CE'])
        ifTrue: [^ NoConversionClipboardInterpreter].
    platformName = 'Win32'
        ifTrue: [^ UTF8ClipboardInterpreter].
        ifTrue: [^ WinShiftJISClipboardInterpreter].
    platformName = 'Mac OS'
        ifTrue: [^ MacShiftJISClipboardInterpreter].
    ^ platformName = 'unix'
        ifTrue: [(ShiftJISTextConverter encodingNames includes: X11Encoding getEncoding)
                ifTrue: [MacShiftJISClipboardInterpreter]
                ifFalse: [UnixJPClipboardInterpreter
        ifFalse: [NoConversionClipboardInterpreter]


この修正のあと、JapaneseEnvironment clearDefault してあげます。



▼ファイルの出力エンコードを UTF8 にする

JapaneseEnvironment >> systemConverterClass
    | platformName osVersion encoding |
    platformName := Smalltalk platformName.
    osVersion := Smalltalk osVersion.
    platformName = 'Win32'
    (platformName = 'Win32'
            and: [osVersion = 'CE'])
        ifTrue: [^ UTF8TextConverter].
    platformName = 'ZaurusOS'
    (#('Win32' 'ZaurusOS' ) includes: platformName)
        ifTrue: [^ ShiftJISTextConverter].
    platformName = 'Mac OS'
        ifTrue: [^ ('10*' match: osVersion)
                ifTrue: [UTF8TextConverter]
                ifFalse: [ShiftJISTextConverter.
    platformName = 'unix'
        ifTrue: [encoding := X11Encoding encoding.
            encoding
                ifNil: [^ EUCJPTextConverter].
            encoding = 'utf-8'
                ifTrue: [^ UTF8TextConverter].
            (encoding = 'shiftjis'
                    or: [encoding = 'sjis'])
                ifTrue: [^ ShiftJISTextConverter].
            ^ EUCJPTextConverter].
    ^ MacRomanTextConverter

やはり修正の後、JapaneseEnvironment clearDefault してあげます。


あと、個人的な好みですが、(FileDirectory oldFileNamed: 'text.txt') edit を do it (alt/cmd + d) して FileList をエディタモードで開いたときなどに便利なように、拡張子が .txt ならデフォルトで UTF8 で開くようにも細工してしまいます。

FileList >> defaultEncoderFor: aFileName
    "This method just illustrates the stupidest possible implementation of
    encoder selection."
    | l |
    l := aFileName asLowercase.
    "((l endsWith: FileStream multiCs) or: [
    l endsWith: FileStream multiSt]) ifTrue: [
    ^ UTF8TextConverter new.
    ]
    "
    ((l endsWith: FileStream cs)
            or: [l endsWith: FileStream st])
        ifTrue: [^ MacRomanTextConverter new].
    (l endsWith: '.txt')
        ifTrue: [^ UTF8TextConverter new].
    ^ Latin1TextConverter new



こうやっていろいろと日本語の入出力が可能になるように直しているうちに、最初に適用した StrikeFontSet>>#copy を含め、umejavaさんのパッチ群に同じものがあることに今更ですがようやく気がついてきたので、まずはこのパッチ群をそのまま使わせていただいて、それを基に手を加えていった方がよさそうな気がしてきました。ということで、いつになるかわかりませんが次回は、試しに4.4J向けのパッチ群を5.0に導入したらどうなるかトライしてみようかと思います。

2015-11-22

[] Squeak5.0で日本語表示をするシリーズ: (寄り道編)ワークスペース内容をHTMLで保存する機能追加

Squeak5.0の日本語フォント表示周りをぼちぼち直していこうかと件のTTなんたらクラスの動きをちまちま調べている途中、ワークスペースにメソッド内容などをコピペしてメモを作ったりしているうちに、シンタックスハイライトできれいに色づけされた文字属性も保持しつつ保存したくなってきたので、思い切ってそういう機能を追加することにしました。そこで、トリビアルなテクニックなどをご紹介しつつ、作業内容を記したいと思います。


もともとワークスペースの内容は、ウインドウメニュー(以前はマウスの第三ボタンでしたが、今はウインドウタイトルバー右手に設置された青いメニューボタンでプルダウン)から save contents to file... を選択したり、黄ボタン(通常は右クリック)メニューからも more... 経由でポップアップできるシフト黄ボタン(同、右クリック)メニューから同名メニュー項目を選択する(ワークスペースを含む TextMorphForEditView なら可能)ことで保存自体はできるようになっています。

http://squab.no-ip.com/collab/uploads/saveContentsInFile01.png


ちなみにワークスペースでは、内容の保存のつもりで accept (alt/cmd + s) しても、これはビューなど UI コンポーネントでの編集内容がモデル(a Workspace)に反映されるだけで、ファイルへの保存がされているわけではないという点に注意を要します。やっかいなことに、こうしてモデルにビューの内容が反映されて一致した状態になってしまうと、両者に不一致がある状態のままインドウを閉じようとした場合に出る警告(Changes have not been saved. Is it OK to cancel those changes?)が出なくなり、当該ウインドウを容易に閉じてしまうことができるというワナももれなくついてくるのでこれまた要注意です。(もっとも、うっかり閉じてしまったとしても、ガベコレされる前なら、Workspace allInstances を inspect it (alt/cmd + i) して、閉じてしまったと思われるワークスペースのモデルをその内容(contents)からなんとか探し、それに openLabel: 'Workspace' などのメッセージを送ってやれば復活は可能です。念のため。)


さて、ワークスペースの内容の保存に話を元に戻すと、たしかに save contents to file... でファイルとして保存することは可能なのですが、保存されるのは文字列のみで、カラー情報などは抜け落ちてしまいます。そこで、保存時の拡張子を .html にしたときにカラー情報などを保持したまま HTML ファイルとしてはき出す細工をします。


まず、メニューから save contents to file... を選んだときにコールされるメソッド(当該機能の実体)を探します。本来であれば StringHolder(ワークスペースの実体)のソースを読むべきなのですが、操作のトリガーとなるメニュー項目をとっ捕まえて訊ねるのが一番手っ取り早いので、ここではそうします。

適当なワークスペースのウインドウメニューかシフト黄ボタンメニューをポップアップさせて、alt/cmd + シフトキーを押しながら当該メニュー項目をクリックしてモーフ(オブジェクト)として選択します。

http://squab.no-ip.com/collab/uploads/saveContentsInFile02.png


他のモーフ同様、メニュー項目オブジェクト(a MenuItemMorph)も選択すると、ハロー(小さな丸いボタン)に囲まれるので、右手にある灰色のデバッグハローをシフトボタンを押しながらクリック(あるいはただクリックしてポップアップするメニューから inspect morph )して選択されたメニュー項目のインスペクターを呼び出します。

http://squab.no-ip.com/collab/uploads/saveContentsInFile03.png


左側のペインからインスタンス変数である selector をクリックして内容を確認すると、#saveContentsInFile であることがわかります。“#”を含めずに save〜以下を選択して implementors of it (alt/cmd + m) すると、同名メソッドの一覧を呼び出せるので、その中から関連がありそうな TextEditor>>#saveContentsInFile をクリックして選択してそのコードを呼び出します。

http://squab.no-ip.com/collab/uploads/saveContentsInFile04.png


最後のところを下に示す青字の部分を赤字のようにちょっと手直しして置き換え、accept (alt/cmd + s) します。いつもどおり、初回の accept でイニシャルを求められたら教えてあげてください。

TextEditor >> saveContentsInFile
    "Save the receiver's contents string to a file, prompting the user for a
    file-name. Suggest a reasonable file-name."
    | fileName stringToSave parentWindow labelToUse suggestedName |
    stringToSave := paragraph text string.
    stringToSave size = 0
        ifTrue: [^ self inform: 'nothing to save.'].
    parentWindow := model dependents
                detect: [:dep | dep isKindOf: SystemWindow]
                ifNone: [].
    labelToUse := parentWindow
                ifNil: ['Untitled']
                ifNotNil: [parentWindow label].
    suggestedName := nil.
    #(#('Decompressed contents of: ' '.gz' ) )
        do: [:leaderTrailer | | lastIndex | "can add more here..."
            (labelToUse beginsWith: leaderTrailer first)
                ifTrue: [suggestedName := labelToUse copyFrom: leaderTrailer first size + 1 to: labelToUse size.
                    (labelToUse endsWith: leaderTrailer last)
                        ifTrue: [suggestedName := suggestedName copyFrom: 1 to: suggestedName size - leaderTrailer last size]
                        ifFalse: [lastIndex := suggestedName
                                        lastIndexOf: $.
                                        ifAbsent: [0].
                            (lastIndex = 0
                                    or: [lastIndex = 1])
                                ifFalse: [suggestedName := suggestedName copyFrom: 1 to: lastIndex - 1]]]].
    suggestedName
        ifNil: [suggestedName := labelToUse , '.text'].
    fileName := UIManager default request: 'File name?' initialAnswer: suggestedName.
    fileName isEmptyOrNil
        ifFalse: [(fileName endsWith: '.html')
                ifTrue: [FileStream
                        newFileNamed: fileName
                        do: [:file | paragraph text printHtmlOn: file]]
                ifFalse: [FileStream
                        newFileNamed: fileName
                        do: [:file | file nextPutAll: stringToSave]]]
        ifFalse: [(FileStream newFileNamed: fileName) nextPutAll: stringToSave;
                 close]


これで #saveContentsInFile で色情報などを保ったまま HTML での保存が可能になります。上のは実は TextEditor>>>#saveContentsInFile の versions の diff(下のペインの内容)を当該メソッドを用いて HTML保存し、それをコピペしたものです。と、書いていて気がついたのですが打ち消し線とか抜け落ちてしまうのですね。残念。


さて、スタイルを保ったまま保存はできましたが、これを読み込んでワークスペースとして表示できないとうれしさは半減です。そこで、ファイルリストをちょっといじって読み込みもできるようにしましょう。

Squeak 組み込みのファイラであるファイルリスト(Tools メニュー → File List などで呼び出し可能)には、ファイル名のリストにあるファイル(たとえば .html ファイル)を右クリックして選択すると同時に黄ボタンメニューを呼び出すとそこに「workspace with contents」という項目が見つかります。

http://squab.no-ip.com/collab/uploads/saveContentsInFile05.png


これを選ぶと選択したテキストファイルの内容を新しいワークスペースとして開けます。ただ、この機能で注意しないといけないのは、ワークスペースとして表示されたのはあくまでファイルの中身のコピーであり、これを編集して accept したからといって、元ファイルに変更が保存されるわけではない、ということです。なお、ファイルの中身を編集して更新したければ、ファイルリストの下のペインか、spawn (alt/cmd + o) して1ペインのエディタスタイルのウインドウを開いて編集し、accept → overwrite that file する必要があります。


話を戻して、workspace with contents ですが、先の方法でこのメニュー項目のインスペクターを開いて selector を見ても #performServiceFor: とあるだけで、今回は残念ながら機能の実体のメソッド名らしき情報を得ることができません。こういう場合は arguments とか target も順に見ていくとよいです。すると target に SimpleServiceEntry: (a FileList --- viewContentsInWorkspace) とあり、セレクター(メソッド名の実体のシンボル)ではありませんが、それっぽい viewContentsInWorkspace というメソッド名らしき情報を得ることができます。

http://squab.no-ip.com/collab/uploads/saveContentsInFile06.png


試しに、選択して implementors of it すると、はたして興味の対象であるファイルの内容をワークスペースとして表示する機能の実体である FileList>>#viewContentsInWorkspace のコードを呼び出すことができます。

http://squab.no-ip.com/collab/uploads/saveContentsInFile07.png


これを次のように編集して accept してコンパイルします。

FileList >> viewContentsInWorkspace
    
"View the contents of my selected file in a new workspace"
    
    
| aStringOrText aName |
    
directory readOnlyFileNamed: self fullName do: [:file |
        
file setConverterForCode.
        
aStringOrText := (file localName endsWith: '.html') ifFalse: [
            
file contentsOfEntireFile
        
] ifTrue: [
            
(HtmlReadWriter on: file) nextText
        
].
        
aName := file localName.
    
].
    
UIManager default edit: aStringOrText withSqueakLineEndings label: 'Workspace from ', aName


この後、ファイルリストで .html を選択し workspace with contents すると、元のカラー属性などを保ったままワークスペースに内容を復元できるはずです。

 
2004 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2005 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2006 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2007 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2008 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 12 |
2009 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2010 | 01 | 02 | 03 | 04 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2011 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2012 | 01 | 02 | 03 | 04 | 05 | 06 | 08 | 10 | 12 |
2013 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 11 | 12 |
2014 | 01 | 02 | 05 | 07 | 08 | 09 | 10 | 11 |
2015 | 04 | 07 | 08 | 11 | 12 |
2016 | 02 | 03 | 06 |

最近のコメント

1. 08/29 squeaker
2. 08/29 ardbeg1958
3. 10/16 umejava
4. 08/26 sumim
5. 08/26 squeaker

最近のトラックバック

1. 05/25 プラグインレスでSVGを表示する「SIE」開発ブログ - メッセージをや...
2. 01/30 no_orz_no_life - Erlangとジャンケン
3. 12/31 檜山正幸のキマイラ飼育記 - J言語、だってぇー?
4. 09/04 Twitter / @atsushifx
5. 07/06 みねこあ - オブジェクト指向 と FizzBuzz

この日記のはてなブックマーク数
1573646