手続き的で冗長な Ruby のコードを Squeak/Pharo Smalltalk の類似機能を活用してよりシンプルに書き換える

こんな感じの“イケてない”と称されるコードを改善する話。

  def total_sales_within_date_range
orders_within_range = []
@orders.each do |order|
if order.placed_at >= @start_date && order.placed_at <= @end_date
orders_within_range << order
end
end

sum = 0
orders_within_range.each do |order|
sum += order.amount
end
sum
end

Rubyのリファクタリングでイケてないコードを美しいオブジェクト指向設計のコードへ改良するための方法 - その1 - ベルリンのITスタートアップで働くジャバ・ザ・ハットリの日記


元記事では、Smalltalk 由来のいわゆる「〜ect系」メソッドの導入によりコードをシンプルに書き換えていますが、もうちょっと Ruby や Rails に備わっている機能を使うことはできないのかなぁ、とリファレンスを紐解きながらこんなふうにしてみました。

  def total_sales_within_date_range
    within_date_range = ->order{ order.placed_at.between?(@start_date, @end_date) }
    @orders.select(&within_date_range).sum(&:amount)
  end


範囲に収まっているかどうかの判定は無名関数(Proc)にして名前を付け、select に渡しています。Ruby の無名関数は Smalltalk のと違い、〜ect系メソッドの引数としてはそのまま渡せないので、& を付ける必要があります。

範囲に収まっているかどうかの判定処理記述の中身についても、Date が Numeric 同様 Comparable なのを利用して簡潔な between? に置き換えています。Smalltalk にも Magnitude>>#between:and: がありますね。


map(&:amount).inject(0, :+) も冗長で意図が伝わりにくいので sum ひとつに置き換えました。ただ、ここで使った sum は Smalltalk の sum とは違って、次のような定義を想定しています。Rails や Ruby2.4 の sum はよく知らないので、こういう動きでなかったらごめんなさい。

class Array
  def sum(zero = 0, &b)
    inject(zero){ | s, e | s + (b ? b[e] : e) }
  end
end


Ruby の制約として残念だったのは、Proc の within_date_range をクエスチョンマークを使って within_date_range? としたかったのが許されなかったところ。メソッド名にすればクエスチョンマークもOKなのですが、そうすると今度は select の引数にするときに記述が面倒になるので痛し痒しですね。



Sak 関数ベンチを Squeak/Pharo Smalltalk で

絶対どっかにありそうだけど、ベンチマーク用関数 fib_m() を考えてみた。

  • fib_m(0 or 1) = 1
  • fib_m(n) = fib_m(n-1) * fib_m(n-2)

Sak 関数と呼んで下さい。

Diary - 2016 July 研究日記


これを Squeak/Pharo Smalltalk で試してみました。

"メソッド版"
Integer compile: 'fibM
    ^self caseOf: {[0]->[1]. [1]->[1]}
        otherwise: [(self-1) fibM * (self-2) fibM]'.

[40 fibM] timeToRun

"Squeak5.0 [msec] => 1987 "
"Pharo5.0 => 0:00:00:02.096 " 
"ブロック版"
| fibM |
fibM := nil.
fibM := [:n |
    n caseOf: {[0]->[1]. [1]->[1]}
        otherwise: [(fibM value: n-1) * (fibM value: n-2)]
].

[fibM value: 40] timeToRun

"Squeak5.0 [msec] => 3025 "
"Pharo5.0 => 0:00:00:02.986 "


Node.js(V8)にこそ僅差で負けていますが、Squeak/Pharo Smalltalk の Cog VM もなかなかの速度をたたき出しています。

$ gcc -o sak_bench sak_bench.c


$ time ./sak_bench
real    0m0.913s
user    0m0.890s
sys     0m0.000s
$ cat sak_bench.js
function fib_m(n){
    if(n == 0) return 1;
    if(n == 1) return 1;
    return fib_m(n-1)*fib_m(n-2);
}

fib_m(40);


$ node -v
v0.10.31


$ time node sak_bench.js
real    0m1.681s
user    0m0.000s
sys     0m0.000s

Ruby の最近のビルドは試していなかったので、ほとんどその存在を忘れかけていた rbenv で 2.4.0-dev をインストールして手元の環境で試してみたところこんな感じになりました。

$ ruby -v
ruby 2.4.0dev (2016-07-03 trunk 55566) [x86_64-cygwin]


$ time ruby sak_bench.rb
real    0m16.726s
user    0m16.133s
sys     0m0.424s


ささださんのところの環境よりちょっとだけ速い結果を出すようですが、言語によって前後するようです。

$ gosh -V
Gauche scheme shell, version 0.9.5_pre1 [utf-8,pthreads], x86_64-unknown-cygwin


$ time gosh sak_bench.scm
real    0m13.953s
user    0m13.905s
sys     0m0.000s
$ python3 -V
Python 3.4.3


$ time python3 sak_bench.py
real    0m50.436s
user    0m50.390s
sys     0m0.015s
$ python2 -V
Python 2.7.10


$ time python2 sak_bench.py
real    0m42.712s
user    0m42.390s
sys     0m0.109s


ついでに Ruby でも Proc 版も試してみます。

$ cat sak_bench_proc.rb
fib_m = ->(n){
  case n
  when 0, 1
    1
  else
    fib_m[n-1] * fib_m[n-2]
  end
}

fib_m[40]


$ time ruby sak_bench_proc.rb
real    0m49.341s
user    0m48.775s
sys     0m0.302s

現在の 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 は、-80 以降の Smalltalk とは言語としてはまったく別物なので注意を要します。

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

Smalltalk-76 には ⇒ を使用する後述の Smalltalk-72 スタイルの if 式の他に、通常の言語にある if式構文が用意されています。つまり Smalltalk-76 では -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)なようです。

「『出現確率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]

平和な動物園を作ろう!をインスタンス特異的メソッドを用いて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

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

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に導入したらどうなるかトライしてみようかと思います。