Java.use(better); Episode#22 ファインダーを作成する
《前の記事|記事一覧|次の記事》
Java.use(better, Scala);
Episode#22ファインダーを作成する:履歴を残す
@父親が、子供達にできる最も重要なことは
@子供達の母親を愛することだ
The most important thing a father can do for his children is to love their mother.
Theodore Hesburgh - Wikipedia
《関連記事》
■ 事例:class UpperCase
次の事例は、その理解を深めるための便宜的なものです。trait Iterator で規定した、少数の抽象メソッド hasNext/next を実現するだけで、多数のメソッド(foreach,map,filter,zip など)を利用できます。
class UpperCase(s: String) extends Iterator[Char] { var c = 0 def hasNext = c < s.size def next() = { val r = s(c).toUpper; c += 1; r } }
UpperCase は、特定の文字列 s を大文字に変換して、各文字を巡回します。
scala> new UpperCase("abc") foreach print ABC
メソッド foreach を利用すると、各要素(大文字)を順に出力できます。また、
scala> for (e <- new UpperCase("abc")) print(e) ABC
for 式でも同じことができます。さらに、
scala> new UpperCase("abcde") ... scala> . foreach { e => if (e.toInt%2 != 0) print(e) } ACE scala> new UpperCase("abcde") filter { _.toInt%2 != 0 } ... scala> . foreach { print } ACE
foreach に代えて、filter を利用すると、特定の条件を満たす要素だけを巡回できます。そして、
scala> new UpperCase("abc") map { _.toInt%5 } ... scala> . foreach { print } 012 scala> print( new UpperCase("abcde") map { | _.toInt%5 | } mkString(",") ) 0,1,2,3,4
メソッド map を利用すると、巡回した要素に特定の関数を適用して得られる要素を列挙できます。
scala> new UpperCase("abc").zip(new UpperCase("12345")) ... scala> . foreach { print } (A,1)(B,2)(C,3) scala> new UpperCase("abcde").zip(new UpperCase("789")) ... scala> . foreach { print } (A,7)(B,8)(C,9)
メソッド zip を利用すると、要素を巡回するときに、他の要素と組み合わせて利用できます。その引数には、Iterator に準拠するものを指定できます。ただし、どちらか一方の要素がないと、他方の要素も無視します。
scala> new UpperCase("abc").zip(1 to 3 iterator) foreach { | case (e,f) => print(e+""+f) | } A1B2C3 scala> print( new UpperCase("abc").zip(1 to 3 iterator) map { | case (e,f) => (e+"")*f | } mkString(",") ) A,BB,CCC
パターンマッチングを利用して、各要素を参照するときには、タプルパターン case(e,f) を指定します。
■ 未来への考察
Scala では、インスタンスを生成するときに、そのブロック内で初期値を設定するだけでなく、任意の処理を記述できるので、コードが簡潔で見通しが良くなります。プログラム(算譜)が大きく複雑になると、プログラミング(作譜)の効率が悪く、バグの温床になりかねないので、この特徴は重宝します。
オブジェクトごとに最適な制御構造を実現するための仕掛が、trait Iterator に用意してあるので、少数の抽象メソッドを実現するだけで、多数のメソッド群を利用できます。というのも、抽象メソッド hasNext/next だけで実現されているので、具象クラスに依存しない抽象表現になっているからです。必要なら、具象クラスでそのメソッドを最適化できます。
オブジェクト指向の醍醐味は「オブジェクトごとに最適な制御構造」を再定義できることです。最適化した制御構造を活用するときには、同じメッセージに呼応して、その状況で最適な問題解決が図れるかで、プログラマーの力量が問われます。それが可能になるのは、制御メッセージの解釈を、コンパイラーに委ねるのではなく「オブジェクト自身が判断する」仕掛を用意できるからです。
料理のレシピがあると、調理する順番に沿った手順の一覧を確認できて便利です。が、実際には、素材の状態に合わせたり、代用となる素材を探したりなど、料理人には、その状況を見極める技能が求められます。それは、プログラマーにも通じるものがあります。
》作業中です《
↑ TOP
Java.use(better); Episode#22 ファインダーを作成する
《前の記事|記事一覧|次の記事》
Java.use(better, Scala);
Episode#22ファインダーを作成する:履歴を残す
@父親が、子供達にできる最も重要なことは
@子供達の母親を愛することだ
The most important thing a father can do for his children is to love their mother.
Theodore Hesburgh - Wikipedia
《関連記事》
┃余録:trait Iterator
trait Iterator を再利用すると、少数の抽象メソッド hasNext/next を実現するだけで、多彩な機能を利用できます。
trait Iterator[+A] extends TraversableOnce[A] { def hasNext: Boolean def next(): A
メソッド hasNext は、まだ巡回していない要素があるかを判定します。次に next を呼び出したとき、要素が得られるなら true を、それ以外は false を返します。メソッド next は、次に巡回する要素を返します。ただし、hasNext が true を返さないときには、その動作を保証しません。
■ メソッド foreach
構造化プログラミングの「条件反復」に代わる、メソッド foreach の動作を規定します。
def foreach[U](f: A => U) { while (hasNext) f(next()) }
注目に値するのは、抽象メソッド hasNext/next だけで実現されているので、具象クラスに依存しない「抽象表現」になっていることです。まだ巡回していない要素がある hasNext かぎりは、次に巡回する要素 next() に特定の関数 f を適用します。これは、構造化プログラミングの「条件反復」に相当します。
while (条件式) 任意の処理(各要素)
つまり、メソッド foreach は、条件を規定する hasNext と、要素を取り出す next に「要素分解」できるのが分ります。
この抽象表現を「テンプレート」と見なすなら、具象クラスごとに最適化されたメソッド hasNext/next の本体が、そこにインライン展開されることに相当します。必要なら、具象クラスごとに最適化したメソッド foreach を再定義します。
■ メソッド map
各要素に関数を適用して得られる、新たな要素を列挙します。
def map[B](f: A => B): Iterator[B] = new AbstractIterator[B] { def hasNext = self.hasNext def next() = f(self.next()) }
ここで、抽象クラス AbstractIterator は、
private[scala] abstract class AbstractIterator[+A] extends Iterator[A]
trait Iterator で規定した機能を具現したいときに利用します。
これも、抽象メソッド hasNext/next だけで実現されているので、具象クラスに依存しない抽象表現です。条件を規定する hasNext は、self.hasNext に代わるものです。要素を取り出す next は、self.next から得られる要素に、特定の関数 f を適用するだけです。
■ メソッド zip
他のイテレーターと組み合わせて、各要素(タプル)に関数を適用して得られる、新たなタプルを列挙します。どちらか一方の要素が見つからないと、他方の要素も無視します。
def zip[B](that: Iterator[B]): Iterator[(A, B)] = new AbstractIterator[(A, B)] { def hasNext = self.hasNext && that.hasNext def next = (self.next, that.next) }
これも、具象クラスに依存しない抽象表現です。条件を規定する hasNext は、self.hasNext および that.hasNext に代わるものです。要素を取り出す next は、self.next および that.next を要素とするタプルを生成します。
》作業中です《
↑ TOP
Java.use(better); Episode#22 ファインダーを作成する
《前の記事|記事一覧|次の記事》
Java.use(better, Scala);
Episode#22ファインダーを作成する:履歴を残す
@父親が、子供達にできる最も重要なことは
@子供達の母親を愛することだ
The most important thing a father can do for his children is to love their mother.
Theodore Hesburgh - Wikipedia
《関連記事》
■ trait Iterator
特定の TreeNode を文字列で表現するために、新たなクラス Node を規定します。この目的に適う制御構造を再定義すると、コードの見通しが良くなります。
15: import javax.swing.tree.TreeNode 16: class Node(node: TreeNode) { 17: override def toString() = displayString(node, 0) 18: def displayString(node: TreeNode, level: Int): String = { 19: val buf = new StringBuffer() 20: buf.append("%s- %s\n".format(tabs(level), node)) 21: nodeIterator(node) foreach { e => 22: buf.append(displayString(e, level+1)) 23: } 24: buf.toString 25: } 26: def tabs(level: Int) = " "*level 29: def nodeIterator(node: TreeNode) = new Iterator[TreeNode] { 30: val iter = node.children 31: def hasNext = iter.hasMoreElements 32: def next() = iter.nextElement.asInstanceOf[TreeNode] 33: } 34: }
[17]オブジェクトを表現する文字列を作成します。メソッド displayString を下請にして、特定のノード node を頂点 0 とするツリーを文字列で表現します。
[18-25]特定のノード node を頂点 level とするツリーを文字列で表現します。[19]バッファー buf を用意して、各ノードの情報を追加します。[20]先行するタブの数 tabs(level) が、そのノードのレベルに対応します。[21]各ノードを巡回するイテレーター nodeIterator を利用して、深さ優先で探索します。[22]再帰呼び出し displayString がツリーの階層構造を反映するので、次のレベルを探索するときに、実引数の値 level+1 を増やします。
[29-33]特定のノード node の子を巡回するために、trait Iterator で規定したメソッドを実現します。[31]メソッド hasNext は、hasMoreElements に代えて、まだ巡回していないノードがあるかを判定します。[32]メソッド next は、nextElement に代えて、次に巡回するノードを獲得します。
■ 事例:class Person
次の事例は、その理解を深めるための便宜的なものです。
class Person(name0: String) { override def toString = "Person(%s%s%s)" format (name, if (birthday == null) "" else " %s" format birthday, if (fellows.isEmpty) "" else ", [%s]" format fellows.mkString(", ")) def name: String = name0 var birthday: (Int,Int,Int) = _ def birth(y: Int, m: Int, d: Int) = { birthday = (y,m,d) } import scala.collection.mutable.ListBuffer val fellows = new ListBuffer[Person] def add(p: Person) { fellows += p } }
特定の情報を参照 name したり、新たな情報を設定 birth したり、関連するオブジェクトを登録 add するメソッドを利用すると、次のようなことができます。
val p = new Person("John Doe") println(p.name) p.birth(1970,1,1) p.add(new Person("A")) p.add(new Person("B"))
新たなインスタンス p を生成すると、その名前を参照できるので、誕生日を設定して、2人の仲間を追加します。
問題は、関係の深いコードの断片を特定するのに「変数 p だけが頼り」ということです。やがて、コードが大きく複雑になると、バグの温床になりかねません。なにか良策はないでしょうか。あります。
Scala では、インスタンスを生成するときに、そのブロック内で初期値を設定するだけでなく、任意の処理を記述できます。
val p = new Person("Jane Doe") { println(name) birth(1970,1,1) add(new Person("A")) add(new Person("B")) }
関係の深い情報が散在せず、同じブロック内にあるだけで、コードの見通しが良くなります。どちらも、次の結果が得られます。
scala> print(p) Person(Jane Doe (1970,1,1), [Person(A), Person(B)])
》作業中です《
↑ TOP
Java.use(better); Episode#22 ファインダーを作成する
┃Iterator:オブジェクトごとに最適な制御構造を規定する
オブジェクト指向プログラミングの醍醐味のひとつは、オブジェクトごとに最適な制御構造を再定義できることです。すると、コードが簡潔で見通しも良くなります。今回は、構造化プログラミングの「条件反復」に代わる、メソッド群を紹介します。
☞ 余録:trait Iterator
□ イベントを処理する
前回に続き、イベント処理を進化させます。今回は、選択した項目を頂点とするツリー情報を表示します。
20: class View(frame: Frame) extends BorderPanel { 43: def update(source: TreeView) { 44: import javax.swing.tree.TreeNode 45: val node = source.selectedComponent.asInstanceOf[TreeNode] 46: frame.title = node.toString 47: textArea.text = "" 48: textArea.append("%s\n" format new Node(node)) 49: textArea.caret.position = 0 50: } 51: }
[43-50]ツリー項目を選択したときに発生するイベントに呼応して、タイトルを設定するとともに、テキスト領域に選択した項目を追記してからキャレットを戻すと、表示した情報を先頭から閲覧できます。ここでは、新たに追加したメソッド selectedComponent(後述)を利用します。
16: class TreeView extends Component { 27: def selectedComponent = peer.getLastSelectedPathComponent 28: }
[27]前述した lastPath との違いは、選択した項目を示す文字列ではなく、それに関連するオブジェクトが得られることです。
》作業中です《
↑ TOP
Java.use(better); Episode#22 ファインダーを作成する
《前の記事|記事一覧|次の記事》
Java.use(better, Scala);
Episode#22ファインダーを作成する:履歴を残す
@父親が、子供達にできる最も重要なことは
@子供達の母親を愛することだ
The most important thing a father can do for his children is to love their mother.
Theodore Hesburgh - Wikipedia
《関連記事》
《目的》同じアプリケーションを異なるプログラミング言語 Java/Jython/Scala で作成する過程(作譜:プログラミング)を通して、言語の違いに依らない概念を適用するための手法(算譜:プログラム)を紹介します。
《動機》ともすると、プログラミング言語の違いに目を奪われて、言語の違いに依らないプログラムの本質を見失いがちです。そこに、同じアプリケーションを異なるプログラミング言語で作成することの意義があります。その本質を理解できれば、新たなプログラミング言語を習得するときに役立ちます。
┃履歴を残す:テキスト領域を利用する
前回に続き、アプリケーションを進化させます。今回は、選択したツリー項目の履歴を残します。
■ アプリケーションを起動する
$ scala cherry.pie.Tips
アプリケーションを起動すると、次のようなウインドーが現れます。
(左)リスト項目を選択すると(上)タイトルが変化して(右)テキスト領域に選択した項目の履歴が表示されます。
選択した項目は追記されるので、すべての履歴を参照できます。表示された情報はプレーンテキストなので、他のアプリケーションでも再利用できます。
■ 既存のコンポーネントに組み込む
既存のコンポーネントに、新たに作成した TreeView を組み込みます。コンポーネントの中には、他のコンポーネントを組み込むための「コンテナ」があります。前述した BorderPanel もそのひとつですが、SplitPane も便利な機能を提供します。これを利用すると、引き戸を左右にずらす感覚で、ウインドー内の限られた領域を有効に活用できます。
20: class View(frame: Frame) extends BorderPanel { 21: self_ => 29: val textArea = new TextArea 30: new SplitPane( 31: Orientation.Vertical, 32: new ScrollPane(treeView), 33: new ScrollPane(textArea)) { 34: import java.awt.Dimension 35: preferredSize = new Dimension(300,200) 36: dividerLocation = 100 37: self_.add(this, BorderPanel.Position.Center) 38: } 40: def update(source: TreeView) { 41: val s = source.lastPath 42: frame.title = s 43: textArea.append("%s\n" format s) 44: } 45: }
[30-38]SplitPane のインスタンスを生成するときに、左右に配置するコンポーネントを指定します。[34-37]初期値を設定するだけでなく、ブロック内に任意の処理を記述できます。関係の深いコードの断片が同じブロック内にあると、プログラミング(作譜)の作業効率が上がります。
☞ 余録:インスタンスを生成する
[40-44]ツリー項目を選択したときに発生するイベントに呼応して、タイトルを設定するとともに、テキスト領域に選択した項目を追記します。今回は、新たに追加したメソッド lastPath(後述)を利用します。
19: class TreeView extends Component { 30: def lastPath = peer.getLastSelectedPathComponent.toString 31: }
[30]選択した項目を示す文字列を作成します。プログラミング(作譜)の作業効率を良くするために、このメソッドを追加しました。アジャイル開発に従事するプログラマーには、必要になるまでメソッドを追加せずに、必要になったらすぐに追加する姿勢が望まれます。
》作業中です《
↑ TOP
Java.use(better); Episode#21 ファインダーを作成する
□ match 式とポリモフィズム
match 式をメソッド呼び出しと見なすなら、その対象に任意のオブジェクトを指定できるので「ポリモフィズム」を体現しているのが分ります。
同じメソッド呼び出しでも、オブジェクトごとに異なる振舞を示します。というのも、そのメソッドをどのように解釈するかを、オブジェクト自身が判断する仕掛を用意できるからです。次のコードの断片
scala> x.run
を見て、どのような様子を想像しますか。
scala> x = new Person; x.run two legs
x が人なら、二本足で疾走する様を想像できるでしょう。
scala> x = new Dog; x.run four legs
x が犬なら、四本足で疾走する様を想像できるでしょう。
scala> x = new Car; x.run wheel tracks
x が車なら、そこに残るタイヤの跡を想像できるでしょう。
scala> x = new Computer; x.run move about
x がコンピューターなら、それが部屋中を動き回るとは想像しないでしょう。世間ではこれを「バグ」と呼びます。
オブジェクト指向の醍醐味は「オブジェクトにメッセージを送る」という原則に沿って、すべての現象を説明できることです。さらに、ポリモフィズムを活用すると、同じメッセージに呼応して、その状況で最適な問題解決が図れます。
scala> val s = List(new Person, new Dog, new Car) ... scala> s foreach { _.run } two legs four legs wheel tracks
この事例では、同じメッセージ run に呼応して、オブジェクトごとに異なる挙動を示します。それが可能になるのは、そのメッセージの解釈を、コンパイラーに委ねるのではなく「オブジェクト自身が判断する」仕掛を用意できるからです。そのことを端的に示したのが、次のコードの断片です。
trait X { def run } class Person extends X { def run = println("two legs") }
あるコミュニティーの中で通用する規則 trait X を逸脱しない範囲でなら、オブジェクトは自由に活動できます。そこで、前述したコードの断片を書き換えると、
scala> s foreach { case x => x.run } ...
ポリモフィズムを期待している状況でも、条件選択は可能です。この case は冗長ですが、そこにコンパイラーが干渉する余地が残ります。
scala> s foreach { | case x: Person => x.run | case x: Dog => x.run | case x: Car => x.run | } ...
ここでは、ポリモフィズムを期待している状況と知りながら、それを無視するかのように、コンパイラーが干渉します。型付きパターンによって条件選択を迫る必要はありません。というのも、同じメッセージ run に呼応して、オブジェクトごとに異なる挙動を示せるからです。
scala> s foreach { any => | any match { | case x: Person => println("two legs") | case x: Dog => println("four legs") | case x: Car => println("wheel tracks") | } | } ...
これを見ると、ポリモフィズムを実現するために、クラスごとに分散させていたコードの断片が、一堂に会して、インライン展開されているかのようです。つまり、case ... => ... という形式は、名前を持たないメソッド呼び出しを、関数リテラルで表現したにすぎないのが分ります。
┃未来への考察
match 式は、ポリモフィズムに貢献するオブジェクト群が集う場を提供するだけで、その問題解決をオブジェクト自身に委ねることに変わりはありません。パターンマッチングに貢献するコンストラクターパターンは、Scala の特徴でもある拡張性〔= scalability〕を端的に示す好例のひとつです。それを象徴するのが、メソッド unapply の存在です。
ポリモフィズムの恩恵には、算譜(プログラム)もさることながら、作譜(プログラミング)によって浴することができます。たとえば、新たな対象として
scala> x = new Stream; x.run
が必要になると、match 式とポリモフィズムとのトレードオフ問題に直面します。このとき、いくつの視座を持ち寄れるかで、プログラマーの力量が問われます。
》作業中です《
↑ TOP
■
$ scala ... scala> trait X { def run } defined trait X scala> var x: X = null x: X = null scala> class Person extends X { | def run = println("two legs") | } defined class Person scala> class Dog extends X { | def run = println("four legs") | } defined class Dog scala> class Car extends X { | def run = println("wheel tracks") | } defined class Car scala> class Computer extends X { | def run = println("move about") | } defined class Computer