Smalltalkのtは小文字です

id:sumim:about

Smalltalk を本格的に勉強する気はないけれど、うんちく程度に知っておきたいなら→Smalltalkをちょっとかじってみたい人のための、チュートリアルまとめ - Qiita

オブジェクト指向の“モヤッと”の正体を知りたくなったらこちらの記事が役に立つかも→id:sumim:20080415:p1 とか id:sumim:20040525:p1


 

2016-08-30

[] Squeak 5.1 をエディタ代わりに使い始めたので気付いたことを記録


ちょっとしたことを書くときに使うようにエディタ代わりに立ち上げている Squeak環境を 4.3J から久しぶりに 5.1 に更新して気付いたこと。



▼ duplicate があっさり殺されていた件

duplicate は、任意のテキストを選択→ alt-shift-d というキー操作で、選択操作の直前のキャレットの位置に選択テキストを複製して挿入する機能なのですが、よりよく使われるであろう debug it にキーアサイン奪われた上に、あろうことかメソッド(#duplicate:)ごと削除の憂き目をみていたようです。ペーストバッファを汚さないので個人的には気に入って使っていた機能だったので、Squeak 5.1 をエディタ代わりに使い始めて、まず最初にこのことに気がつきました。残念なことです。

カット、コピー、ペースト、アンドゥなどと違い、duplicate は後述の alt-j のモードレス検索や alt-e のスワップ機能(選択テキストを直前の選択テキストと交換)と共に、Mac/Win に継承されず、知らない人は一生知る機会もなく、ともすれば不要と思われがちな編集機能三兄弟なので、again のモードレス検索が殺されそうになったときにこちらも同時に気をつけておくべきでした。ということで、アラン・ケイがむかし Scheme で書いたものを移植したと言われる手書き認識(!?)が外されて以降、空席のままになっているっぽい alt-r に復活させました。

同様に、alt-shift-z を redo に奪われた #makeCapitalized: についても、あればあったで地味に便利なので、こちらも alt-shift-q あたりに復活しておきました。




▼ alt-shift-r、alt-shift-l による編集行もしくは選択行一括インデント/アウトデントのキーアサインが tab、shift-tab に変更&選択時のみ機能に制限

alt-shift-r、l が使えないことに気付いて、さすがにこの機能がなくなるとも思えないので、どうするのかと思ったら、1行以上の複数行を選択して tab 、shift-tab を押すのに変わっていました。alt-shift-r は行を選択しなくても機能していたので1行のみの場合に限っては不便だったのでその意味では改悪ですが、まあ許容範囲でしょうか。なお当該機能は今回のキーアサイン変更の前から行内の文字数が1文字だと機能しないバグがあるのでフィックスしておきました。


ちなみに空いた alt-shift-r、alt-shit-l はそれぞれ Recent Submissions 、File List の呼び出しに転用された模様です。alt-k(ワークスペース起動)、alt-t(トランスクリプト呼び出し)などと違い、テキスト編集中でも使える類似の「何かを起動する」タイプのキーショートカットの一覧はこちら。


    • alt-shift-l → File List
    • alt-shift-o → Monticello Browser
    • alt-shift-p → Preference Browser
    • alt-shift-r → Recent Submissions

alt-k、alt-t など、デスクトップをクリックするなどしてキーボードのフォーカスを明示的に外せば使えるショートカットを含め、PasteUpMorph>>#defaultDesktopCommandKeyTriplets で確認できます。



▼ again (alt-j) によるモードレス検索が alt-g に移動

これはすでに以前 Qiita で書いた通り。検索テキストをダイアログボックスなど UI をいっさい使わずに、おもむろにタイプした文字列を again (alt-j)で検索できる機能が Apple Smalltalk-80 時代からあり、Squeak にも継承されたこの機能を知って以来ずっと愛用していたのですが、Squeak 5.0 からこの使い方は again からは分離され、代わりに find the current selection again (alt-g) に引き継がれました。削除を含むテキスト置き換えの操作を繰り返す again 機能はそのまま alt-j(一括は alt-shift-j )で使えます。

余談ですが、新しい again では、一度 alt-j(alt-shift-j)をタイプしてヒットしたパターンが選択状態になってからもう一度 alt-j(同じく alt-shift-j) をタイプすると、改めて再度置換(同、一括置換)になるという二段階になったので要注意です。



▼ alt - 1〜4 のフォントサイズ変更にいつの間にかなんか変な機能が割り振られているw

もっとも alt-1〜5 のフォントサイズ変更の機能にはバグがあっていまままで正常に動作していなかったので潰されても文句は言えませんが…。^^; 新しい機能は、ブラウザのコードペイン(つまりメソッド定義時)での使用を念頭においた機能で、一行目にメッセージパターンとして宣言された仮引数をキャレットの位置に挿入する機能。何番目の仮引数を挿入するかが 1〜4 に対応しています(SmalltalkEditor>>#typeMethodArgument:)。

これはこれで便利な機能なのかもしれませんが、メソッド定義時以外は意味をなしませんし(実際、ワークスペースなどで普通の文章を打っているときに試したときは何が起こっているか分かりませんでした)、伝統的な alt-1〜4 によるフォントサイズ変更(オリジナルの Smalltalk-80 ではフォント変更の機能も兼ねていた)を潰してしまうのもけしからんことなので、コードペインのときだけ機能するように細工しました。せっかくなので alt-1〜5 も機能するように修正しました。ほぼ使わないとは思いますが、コードペインでも alt-shift-1〜4 で通常の alt-1〜4 の機能も使えるようにもしておきました。




▼ .txt のエンコーダーを utf8 に、.html で保存、読み込みの機能を拡張

エディタ代わりに使うときに、開いてからいちいちエンコーダーを utf8 に変えるのは面倒なので、.txt のときは自動的に UTF8TextConverter を選ぶように細工します。

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.


あと、これもすでに以前書いたものですが、せっかくフォントサイズを変えたりカラーを変えても保存する手段がないのも悲しいので、ワークスペースなどを右クリック→ more... → save contents to file... で .html 付きで保存したときに HTML 出力する機能と、それを読み込んだ(File List の右クリックで workspace with contents)ときプロパティを再現する機能を追加しました。




▼オーサーイニシャルのペーストが undo できないバグがあったので修正

alt-shfit-v でオーサーイニシャルをタイムスタンプ付きでペーストする機能があります。前述の duplicate を復活させている作業で気がついたのですが、どうも多段階 undo がデフォになって undo の機構が変わった影響か、duplicate 同様、オリジナルの実装のとおり #replace:with:and: だけだとうまく取り消し機能が働かないようです。実際のところ、オーサーイニシャルのペースト機能自体を意識して使うことはほぼないのですが、ペーストのつもりで誤ってこの機構が機能したときに undo できないと腹立たしいので直しておく方が精神衛生上よろしかろうと。


TextEditor >> pasteInitials: aKeyboardEvent
    "Replace the current text selection by an authorship name/date stamp; invoked by cmd-shift-v, easy way to put an authorship stamp in the comments of an editor."

    self insertAndCloseTypeIn.
    self openTypeIn.
    self replace: self selectionInterval with: (Text fromString: Utilities changeStamp) and: [self selectAt: self stopIndex].
    ^ true



▼TrueType フォントの選択可能サイズを追加する機能を追加

エディタとしてはあまり必要ありませんが、将来的にプレゼンツールとして使う場合に備えて。Font Chooser のフォントサイズ枠の右クリックメニューと StrikeFont fromUser のポップアップ中のサイズサブメニューの new size 選択時に機能します。



▼querySymbol:(候補がマルチキーワードセレクタ時)と argAdvance:(コロンの直後)が移動させるキャレット位置の調整

Squeak には Apple Smalltalk 時に実装された alt-q で入力しかけのクラス名、セレクタ、変数名等をシンボルから探して補完する query 機能がまだ残っています。最近、新しい undo/redo システムに対応するため再実装されたのですが、その際に複数のキーワードからなるセレクタの場合であってもキャレットが最後に移動してしまうバグが生じていて使いにくかったので修正しました。

ちなみに query はこんな感じに操作や機能します。

https://www.youtube.com/watch?v=jYOEZVnF9eI


また、この query による補完とコンボで用いると便利な advance という機能(次のコロン+スペースの位置にキャレットをジャンプさせる)もいつの間にかコロン+スペースではなく、コロンの直後にキャレットを移動させるように変更されてしまっていたので元に仕様にリバートしました。

2016-08-29

[] Squeak 5.1 でとりあえず日本語を表示させるために踏んだ手順の記録


Windows 8.1 での手順を記します。

  • Squeak5.1 を http://squeak.org/ の Windows版ボタンをクリックしてダウンロード、展開。
  • http://www.geocities.jp/ep3797/modified_fonts_01.html から komatuna.ttf、komatuna-p.ttf を入手して Squeak5.1-16548-32bit.ja.image と同階層に作った fonts フォルダにコピー。
  • Squeak5.1 を起動。初回起動時の Configure は数が多いので面倒なら Skip(あるいは馴染みの設定だけして Done )。
  • sq51fix_JapaneseLocale-sumim.cs を Squeak のデスクトップにドロップインするなどして install 。
  • Tools → Workspace でワークスペースを開く。
  • Locale switchToID: (LocaleID isoLanguage: 'en'); currentPlatform: (Locale isoLanguage: 'ja') をワークスペースにタイプするかコピペして do it してロケールを変更
  • Apps → Font Importer から Komatuna、Komatuna-P をそれぞれ右クリック→ Link Font 。
  • 次のスクリプトを同じくワークスペースにタイプするかコピペして全選択後 do it 。
| font |
font := StrikeFont familyName: 'Komatuna P' pointSize: 12.
Preferences class selectors
   select: [:sel | (sel beginsWith: 'set') and: [sel endsWith: 'FontTo:']]
   thenDo: [:sel | Preferences perform: sel with: font].
font := StrikeFont familyName: 'Komatuna P' pointSize: 9.
Preferences setPaintBoxButtonFontTo: font.
Preferences setBalloonHelpFontTo: font.
BalloonMorph setBalloonFontTo: font
  • save as... などでイメージを保存。

2016-08-23

[] 『プログラミングElixir』出版記念: Elixir、RubySqueak Smalltalkでspawn/chain.exの速度対決



なぜか Ruby インタプリタ開発者が翻訳をしたことで話題の『プログラミング Elixir』 p.167 にある「14.2 プロセスのオーバヘッド」のサンプルコード


これと似たようなことを Ruby の軽量スレッド(Fiber)と Squeak Smalltalk のプロセスでチャレンジしてみようという試みです。もちろん、Elixir や Erlang のプロセスとはいろいろ違うので、かなり大雑把に似たような処理…ということでご勘弁ください。^^;


ちなみに手元の Elixir では spawn/chain.ex の結果はこのようになりました。

$ elixir -v
Erlang/OTP 19 [erts-8.0] [64-bit] [smp:4:4] [async-threads:10]

Elixir 1.3.1


$ elixir -r spawn-chain.ex -e "Chain.run(10000)"
{62000, "Result is 10000"}


$ elixir -r spawn-chain.ex -e "Chain.run(40000)"
{156000, "Result is 40000"}


$ elixir -r spawn-chain.ex -e "Chain.run(100000)"
{438000, "Result is 100000"}


$ elixir -r spawn-chain.ex -e "Chain.run(400000)"
12:31:03.936 [error] Too many processes
** (SystemLimitError) a system limit has been reached


$ elixir --erl "+P 1000000" -r spawn-chain.ex -e "Chain.run(400000)"
{1609000, "Result is 400000"}


$ elixir --erl "+P 1000000" -r spawn-chain.ex -e "Chain.run(1000000)"
{4344000, "Result is 1000000"}

4万で 156ミリ秒 、40万で 1.61秒、100万で 4.34秒とはさすがです。



▼ Squeak Smalltalk 版

残念ながら Squeak/Pharo には Io のようなアクター(あるいはメッセージの非同期通信)機能は組み込みではない上、プロセス(通常の言語でいうところのスレッド)についても resume の際に後述の Ruby の Fiber のように引数を与えることができないため、 Elixir の spawn/chain.ex の動きをストレートには再現できません。

そこで、SharedQueue(next を受け取ると、他のプロセスから nextPut: 等でエレメントがプッシュされるまでアクティブプロセスを停止)をメッセージキューに見立てたアクターっぽい機構で似たような動きを再現してみました。なお、Squeak/Pharo ではブロック(無名関数。[] で処理を括ったもの)に fork というメッセージを送ると、アクティブプロセスと同じ優先度で新しいプロセスが立ち上がるしくみになっています(今回は使いませんが、優先度を変更するには forkAt: を用います。ちなみに優先度が違うプロセス同士の並行処理はノンプリエンプティブっぽく振る舞います)。


| N time ans |
N := 40000.
Smalltalk garbageCollect.
time := [
   | me last |
   me := SharedQueue new.
   last := (1 to: N) inject: me into: [:sendTo :dummy |
      | mbox |
      mbox := SharedQueue new.
      [sendTo nextPut: mbox next + 1] fork.
      mbox
   ].
   last nextPut: 0.
   ans := me next
] timeToRun milliSeconds.
^{time. 'Result is ', ans printString}
=> {382 . 'Result is 40000'}
=> {3526 . 'Result is 400000'}

結果は、4万で 382ミリ秒、40万で 3.53秒(Squeak5.0 で計測。Squeak では Workspace、Pharo なら Playground へのコピペ → print it で動作します)。もちろん、Squeak/Pharo Smalltalk のプロセスは Elixir のそれと比べて限定的なので、あくまで参考値ではありますが、よく健闘してます。しかし試してみたところ 70万プロセスになると VM が落ちます。無念。



▼ Ruby 版

Squeak/Pharo のプロセスとは違い Ruby の軽量スレッドである Fiber は、引数を受け取ることができます(今回は使いませんでしたが返値も返せます)。そこで Ruby 向けには Squeak/Pharo とは別のアプローチで spawn/chain.ex と似たような動きになる処理を書いてみました。なお、計時結果の数値の単位は秒です。

$ cat spawn-chain.rb
require 'benchmark'
require 'fiber'

n = ARGV[0].to_i
n = 10 if n == 0
ans = 0
time = Benchmark.realtime{
  me = Fiber.new{ |n| ans = n }
  last = (1..n).reduce(me){ |send_to,_|
    Fiber.new{ |n| send_to.transfer(n + 1) }
  }
  last.resume(0)
}
p [time, "Result is #{ans}"]


$ ruby -v
ruby 2.4.0dev (2016-08-22 trunk 55983) [x86_64-cygwin]


$ ruby spawn-chain.rb 40000
[22.913834010018036, "Result is 40000"]

結果は 4万で 23秒とふるわず。ちなみに 5万以上ではエラーで計測不能でした。Ruby3 での高速化に期待したいところです。

2016-07-24

[] イケてないRubyのコードのリファクタリングって奴をSmalltalkでやってみる(sumim版)


件のネタについては Ruby ではすでに言及済みですが、その後 えせはらさんが Smalltalk に書き直すかたちで



というすばらしいエントリーを公開してくださいましたので、触発されて私も二番煎じではありますが、同じ Smalltalk 処理系の Pharo を使ってやってみました。ただバージョンは、先頃リリースされたばかりの 5.0 を使用しました。あとで出てきますが、新機能のサジェスチョンも初めて体験できて面白かったです。


正直このネタに関しては、肝心のリファクタリング自体は冗長な記述をまとめるだけであっさりと終わってしまいそうなので、このエントリーでは本論から少し離れて、その前段階の Ruby のリファクタリング対象のコードとそのテストをできるだけ忠実に Pharo Smalltalk で再現して実際にテストを通すまでの作業も少し詳しく書きたいと思います、できればお手元でも Pharo をインストールし、同じ作業を試してみるなどして、Smalltalk 体験のとっかかりとなれば嬉しいです。



▼システムブラウザ(クラスブラウザ)の起動

Pharo を起動すると仮想的なデスクトップが現われるので、このデスクトップに相当する領域でクリック→ポップアップするメニューから System Browser を選択してシステムブラウザ(たんにブラウザ、あるいはクラスブラウザともいう)を起動します。

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

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


通常、IDE によるサポートがデフォの Smalltalk では、このクラスブラウザを使って既存のクラスやメソッドの閲覧や修正、新規のものの追加を行ないます。

Smalltalk が Ruby などの通常の言語処理系と大きく違うのは、組み込みクラスはもちろん、コンパイラなど処理系の主要部や IDE それ自体が Smalltalk のオブジェクトで構成され(セルフホスティング)、なおかつ、それらを構成するオブジェクトをオブジェクトストア(簡易OODB。仮想イメージと呼ばれるファイルに適宜保存可能なオブジェクトメモリ)内に生きたまま永続化可能なかたちで存在させ運用しているところです。

したがって Smalltalk において、クラスやメソッドの定義はソースコードの記述というよりは、その場で「クラスやメソッドのオブジェクトを生成してオブジェクトストアに追加する(あるいは差し替える)作業」という感覚であることは、他言語とかなり違います。ここらへんは Smalltalk でこの仕組みを意識しつつ体験しないと実感できないところなので、ぜひ実際に操作してつかんでほしいところです。



▼Order クラスの定義

元の Ruby のコードでは、さくっとこのような定義があります。

class Order < OpenStruct
end

残念ながら Smalltalk には Ruby の OpenStruct のような便利な機構はないので、普通に amount と placed_at をインスタンス変数に持つ Object のサブクラスとして定義します。

いくつか方法はあるのですが、ここでは先ほど起動したクラスブラウザの上段左端の枠内をスクロールして最後にある _UnpackagedPackage を見つけてクリックします。すると、下の枠にクラス定義のテンプレートのようなものが現われるので、その中の #NameOfSubclass を #Order に、instanceVariableNames:キーワードの引数を 'amount placed_at' に、package:キーワードの引数の '_UnpackagedPackage' を 'RubyRefactoring-Example' に変更してから同じ枠内で右クリックメニューから Accept (あるいはキーボードで ctrl + s をタイプ)します。

Object subclass: #Order
	instanceVariableNames: 'amount placed_at'
	classVariableNames: ''
	package: 'RubyRefactoring-Example'

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


すると、上段左から二番目の枠内に Order が現われ、システムに当該クラスが追加されたことが示されます。

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


引き続き、インスタンス変数 amount と placed_at それぞれのアクセッサーを追加しましょう。

Ruby の attr_accessor 相当のことをすればよいわけですが、ここでも Ruby とは違い Smalltalk では、スクリプト内にアクセッサーの自動生成を処理として記述するのではなく、あらかじめクラスブラウザなどで作業として済ませておく必要があります。

やはりいくつか方法はありますが、ここでは上段左から二番目の枠で Order クラスを(必要なら一度クリックして選択解除した後、改めて)右クリックしてポップアップするメニューから Refactoring → Class Referctoring → Generate Accessor → OK でアクセッサーを自動的に生成、追加します。

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


このタイミングで、次のような「Author Identification」ダイアログが現わて名前の入力を促されることがあります。ここで入力した名前は、システムに改変を加えた編集者名としてメソッド定義等のバージョン管理などをするのに利用されるので適当に入力して OK してやってください。

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


今さらですが、デフォルトでアンダーラインを変数名などに使えるのは、Ruby のコードを移植するときに便利ですね。

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



▼OrdersReport クラスの定義

Orderクラスと同様の操作で、引き続き OrdersReport クラスとそのアクセッサーを定義します。Ruby の定義は次のようなものです。

class OrdersReport
  def initialize(orders, start_date, end_date)
    @orders = orders
    @start_date = start_date
    @end_date = end_date
  end

  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
end


直前の Orderクラスおよびそのアクセッサーの追加(定義)作業の直後であれば、クラスブラウザの上段左端の枠ではすでに RubyRefactoring-Example パッケージが追加されて選択状態にあるので、今回は package:キーワードの引数はそのままで結構です。(必要なら、Orderクラスをクリックして選択解除してクラス定義のテンプレートを下の枠に呼び出してから)subclass: キーワードの引数を #OrdersReport に、instanceVariableNames: の引数を 'orders start_date end_date' に変更して Accept します。

それぞれのインスタンス変数のアクセッサーも Order 同様に Generate Accessor で自動生成・追加します。

Object subclass: #OrdersReport
	instanceVariableNames: 'orders start_date end_date'
	classVariableNames: ''
	package: 'RubyRefactoring-Example'

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


OrdersReportクラスには、加えてメインの total_sales_within_date_range メソッドも追加してあげる必要があります。クラスブラウザ上段左から三番目の枠内の accessing プロトコルをクリックして選択すると下にメソッド定義のためのテンプレートが現われるので ctrl + a などで選択後、次のコード(OrdersReport >> より後、total_sales_within_date_range 以下を使用します。以降に出てくるコードも同様です)をコピペ、あるいはタイプして入力して置き換えてください。

OrdersReport >> total_sales_within_date_range
	| orders_within_range sum |
	orders_within_range := OrderedCollection new.
	orders
		do: [ :order | 
			(order placed_at >= start_date and: [ order placed_at <= end_date ])
				ifTrue: [ orders_within_range add: order ] ].
	sum := 0.
	orders_within_range do: [ :order | sum := sum + order amount ].
	^ sum

入力が完了したら、同枠内で右クリック → Accept (コンパイルしてクラスにメソッドを追加。なお、Smalltalk ではコンパイルはメソッドごとにインクリメンタルに行なわれる)します。コンパイルが通ると上段右端の枠内に total_sales_within_date_range が現われ、以降、いつでも当該メソッドのソースコードを読んだり編集したりが可能になります。

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



▼OrdersReportTest クラスの定義

最後に Ruby 版のテストを Pharo Smalltalk に移植します。Ruby では次のようなコードでした。

require 'spec_helper'

describe OrdersReport do
  describe '#total_sales_within_date_range' do
    it 'returns total sales in range' do
      order_within_range1 = Order.new(amount: 5,
                                      placed_at: Date.new(2016, 10, 10))
      order_within_range2 = Order.new(amount: 10,
                                      placed_at: Date.new(2016, 10, 15))
      order_out_of_range = Order.new(amount: 6,
                                     placed_at: Date.new(2016, 1, 1))
      orders = [order_within_range1, order_within_range2, order_out_of_range]

      start_date = Date.new(2016, 10, 1)
      end_date = Date.new(2016, 10, 31)

      expect(OrdersReport.
             new(orders, start_date, end_date).
             total_sales_within_date_range).to eq(15)
    end
  end
end

RSpec のコードですが、こちらも単純に Pharo/Squeak Smalltalk に組み込みの xUnit 系 TestCase を継承した OrdersReportTest クラスに test_total_sales_within_date_range メソッドを定義することで再現とすることにします。

(Order または OrdersReportクラスが選択状態にあるならクリックして選択を解除した状態で)Object を TestCase に、subclass: キーワードの引数を #OrdersReportTest に変更して Accept します。上段左から三番目の枠に OrdersReportTest が現われることを確認してください。

TestCase subclass: #OrdersReportTest
	instanceVariableNames: ''
	classVariableNames: ''
	package: 'RubyRefactoring-Example'

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


このクラスにはインスタンス変数はないのでアクセッサーの自動生成作業は不要です。あとはテストメソッド test_returns_total_sales_in_range を追加します。

まず、上段左から三番目の枠の no message を選択して下の枠にメソッド定義のテンプレートを呼び出します。下の枠内で ctrl + a でテンプレート全選択後、次のコードに置き換えてから Accept してコンパイルを完了します。

OrdersReportTest >> test_returns_total_sales_in_range
	| order_within_range1 order_within_range2 order_out_of_range orders start_date end_date |
	order_within_range1 := Order new amount: 5; placed_at: (Date year: 2016 month: 10 day: 10).
	order_within_range2 := Order new amount: 10; placed_at: (Date year: 2016 month: 10 day: 15).
	order_out_of_range := Order new amount: 6; placed_at: (Date year: 2016 month: 1 day: 1).
	orders := {order_within_range1. order_within_range2. order_out_of_range}.
	start_date := Date year: 2016 month: 10 day: 1.
	end_date := Date year: 2016 month: 10 day: 31.
	self
		assert:
			(OrdersReport new orders: orders; start_date: start_date; end_date: end_date)
				total_sales_within_date_range
		equals: 15

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



▼テストの実行

上段左から二番目の枠でテストクラス OrdersReportTest を、あるいは上段右端の枠で実行したいテストメソッド test_returns_total_sales_in_range を(必要ならクリックして選択解除後、改めて)右クリックしてポップアップメニューから Run tests を選ぶとテストが走ります。上記の作業がうまく終わっていればグリーンが左下に現われるはずです。

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



▼リファクタリング1: a >= b and: [a <= c] の代わりに a between: b and: c を使いましょう

ようやく本題です。と、意気込みたいところですが、ここまででほぼ力尽きたので以下はあっさり目に。^^;


リファクタリングの例題ということで、あえてそうしているのでしょうが、とにかく total_sales_within_date_range というメソッドの記述内容は冗長で手続き的です。Smalltalk や Ruby、Rails に元から備わっている API をあえて無視して書かれているだけ、という言い方もできます。裏を返せば、Smalltalk や Ruby、Rails をちゃんと使えば読み下しやすくすっきり書けるということです。

まず、Pharo 5.0 から導入された下段の枠に表示されるコードのサジェスチョンに従った変更をしてみます。

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


最初の "a >= b and: [a <= c]" -> "a between: b and: c" というのは特には説明は不要かと思います。Ruby にも between? がありますね。

このサジェスチョンの脇の?と×の間には「Automatically resolve the issue」ボタンが用意されているので、これを押して自動で処理してもらいましょう。

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

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

OrdersReport >> total_sales_within_date_range
	| orders_within_range sum |
	orders_within_range := OrderedCollection new.
	orders
		do: [ :order | 
			(order placed_at between: start_date and: end_date)
				ifTrue: [ orders_within_range add: order ] ].
	sum := 0.
	orders_within_range do: [ :order | sum := sum + order amount ].
	^ sum

テストももちろん通ります。以後、OrdersReportTest と OrdersReport を行き来するのは面倒なので、OrdersReport を右クリックして Browse full してクラスブラウザを2つ開いておくと total_sales_within_date_range を書き換えた後のテストが楽になるのでお薦めです。

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


あるいは、ブラウザ右上の Go back、Go foward ボタンを使って行き来するのでもよいでしょう。



▼リファクタリング2: do: の代わりに select: や collect: を使いましょう

こちらも Pharo からのサジェスチョンのひとつなのですが、慣れていないとちょっとイメージしにくいですね。条件に合致したものをあらかじめ用意した追加可能なコレクション(この場合 an OrderedCollection)に加えていくという処理は select:(条件に合致したものを排除するなら reject:)を使った処理に置き換えられます。

OrdersReport >> total_sales_within_date_range
	| orders_within_range sum |
	orders_within_range := orders
		select: [ :order | order placed_at between: start_date and: end_date ].
	sum := 0.
	orders_within_range do: [ :order | sum := sum + order amount ].
	^ sum


▼リファクタリング3: sum := 0. colln do: [:x | sum := sum + x ]. ^sum は inject:into: に置き換えましょう

上の select: と考え方は似ていますが、こちらはいわゆる畳み込み処理です。Smalltalk の inject:into: は Ruby の inject に相当し、第一引数として初期値を与え、それと最初の要素との何かしらの演算をし(二引数のブロックで与えられる)を次の要素との同じ演算処理に受け渡す処理を表現するのに用います。

件のコードでも sum に初期値 0 を与え、ループを回して sum に足し込んでいく様子は、inject:into: のターゲットとなる処理の代表格です。

OrdersReport >> total_sales_within_date_range
	| orders_within_range sum |
	orders_within_range := orders
		select: [ :order | order placed_at between: start_date and: end_date ].
	^ orders_within_range inject: 0 into: [ :sum :order | sum + order amount ]


▼リファクタリング4a: orders_within_range の amount の sum であることが分かる記述にする(その1)

当初の冗長さから比べればだいぶ簡潔になったのですが、inject:into: というのは select: などに比べると、読み下しのしやすさという意味からはちょっと弱いです。ここはやはり、orders_within_range の amount の sum を欲していることがわかる記述にしたいところです。

ひとつの解決策としては、組み込みの select:thenCollect: を使う方法です。

OrdersReport >> total_sales_within_date_range
	^ (orders
		select: [ :order | order placed_at between: start_date and: end_date ]
		thenCollect: #amount) sum

select:thenCollect: は読んで字の通り、select:キーワードの引数(つまり第一引数)のブロックの処理に合致する要素を選び、さらにそれらについて thenCollect:キーワードの引数(同じく第二引数)のブロックの処理をした値を集めたコレクションを返します。

なお、Squeak/Pharo では、ブロックで記述する処理が要素に対する単項メッセージ(引数を取らないメッセージ)の場合、そのメッセージのセレクタ(メソッド名。実体はシンボル)で代替可能です。この例では [:order | order amount] は #amount に置き換え可能であることをさします。


ただこの select:thenCollect: には問題があって、それは、select:thenCollect: の実装が素朴に select: した結果を collect: しているだけで工夫がないため、ひとつ前のリファクタリング前のコードより、無駄なコレクションを作成するコストがかかってしまうことです。



▼リファクタリング4b: orders_within_range の amount の sum であることが分かる記述にする(その2)

コストの問題を解決するには、inject:into: の sum に特化したエイリアスを定義する方法があります。具体的には Ruby2.4 以降の sum のように、ブロックを引数として取ることができるようにすればよいでしょう。inject:into: と同じ Collection に次の sumOf: メソッドを生やしてやります。


まず念のため inject:into: がどこに定義されているのかを調べます。文字を入力できる場所ならどこでもいいのでその中の(たとえば total_sales_within_date_range のコード入力枠の)空行におもむろに inject:into: とタイプして入力し、ctrl + m(あるいは右クリックメニューから Code search... → Implemnetors of it を選択)します。すると開いたウインドウの上のリスト枠から inject:into: が Collection と LayoutCell というふたつのクラスに定義され、Collection での定義が下のペインに呼び出されます。

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


ここで、下のペインの inject:into: の定義を ctrl + a などの操作で全選択し、次のコードに置き換えたあと Accept (コンパイル)します。なお、クラスブラウザでのコード編集はメソッド名が変われば別のメソッドの追加と解釈されるので、こうした操作で inject:into: が sumOf: に置き換えられてしまう心配はありません。

Collection >> sumOf: block
	^ self inject: 0 into: [ :sum :each | sum + (block value: each) ]

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


あと、このままですと RubyRefactoring-Example パッケージをソースコード単位で扱うとき(たとえば File Out して別の仮想イメージに Load 等した場合)に sumOf: が追加されず困るので、RubyRefactoring-Example パッケージに含めておくのがよいと思います。操作は、sumOf: を選択して右クリック→ Code search... → implementors of it → Browse → 上段右端の枠で sumOf: を右クリック→ Move to package... → RubyRefactoring-Example を選択 → OK です。


以降はこの sumOf: が使えるので、total_sales_within_date_range も次のように書き換えます。

OrdersReport >> total_sales_within_date_range
	^ (orders select: [ :order | order placed_at between: start_date and: end_date ])
		sumOf: #amount

いかがでしょう。指定した期間内の order の amount の sum であることが分かりやすくなったのではないでしょうか。当初のコードと比べると一目瞭然です。

OrdersReport >> total_sales_within_date_range
	| orders_within_range sum |
	orders_within_range := OrderedCollection new.
	orders
		do: [ :order | 
			(order placed_at >= start_date and: [ order placed_at <= end_date ])
				ifTrue: [ orders_within_range add: order ] ].
	sum := 0.
	orders_within_range do: [ :order | sum := sum + order amount ].
	^ sum

なお、クラスブラウザの上段右端の枠内の total_sales_within_date_range を右クリック→ Versions で、リファクタリングしたコードの変遷をたどることができます。

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

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


Smalltalk は環境それ自体がオブジェクトストアのような性格を有していることを利用して、このようなメソッド単位でバージョン管理が可能な SCM 機構が 1980年代から組み込まれて利用されてきています。他にもいろいろと興味深いしくみがあるので、他言語ユーザーの皆さんも、処理系や IDE 等の環境が Smalltalk でどのように実現されているか、ぜひオブジェクトのスープの中に飛び込んで異世界を愉しんでみてください。

2016-07-18

[] 手続き的で冗長な 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 - Ruby on Railsのビシバシはぁはぁ日記

元記事では、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 の引数にするときに記述が面倒になるので痛し痒しですね。


 
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 | 07 | 08 |

最近のコメント

1. 06/25 sumim
2. 06/25 山田
3. 08/29 squeaker
4. 08/29 ardbeg1958
5. 10/16 umejava

最近のトラックバック

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

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