おがさわらなるひこのオープンソースとかプログラミングとか印刷技術とか

おがさわらなるひこ @naru0ga が技術系で興味を持ったりなんだりしたことをたまーに書くブログです。最近はてなダイアリー放置しすぎて記事書くたびにはてな記法忘れるのではてなブログに移行しました。

クリエイティブ・コモンズ・ライセンス
特に断りがない場合は、本ブログの筆者によるコンテンツは クリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンスの下に提供されています。

ちょっとだけScala(その2)

文句いいつつ Scala ってみたわけですが、REPL だとデバッグ手段がないっぽいことが判明して悲しくなった。
ので Web 調べたら Eclipse とか NetBeans とか使えってあったのでその奮闘記。Eclipse 使うのなんか鼻毛抜くより簡単だぜって人には多分退屈なので読まなくていいです。

環境設定

とりあえずもしバージョン古くて困るようなことがあれば考えようと思いつつ、EclipseUbuntu 9.10 の公式レポジトリにある奴。

$ aptitude show eclipse
パッケージ: eclipse
状態: インストール済み
自動的にインストールされた: no
バージョン: 3.5.1+repack~1-0ubuntu3
...

Scala の Plugin は Scala 公式ページ から Software -> IDE and Editor Plugins -> IDE Plugins から Scala IDE for Eclipse をとってきてインストール。まだ発展途上らしいけど Nightly を試す勇気はなかった。

ビルドできねー!

とりあえず File -> New -> Scala Project で Example Scala というプロジェクトをこさえて、File -> New -> File で src の下に exercise-7.2.2.scala ってファイルを作ってさっきの内容をコピペ。

……で、どうすんの?

とりあえずよくわからんからセーブしてみた。ら。
「トップレベルでメソッドの def はできないよーん」
とか怒られてみましたよ (T_T)

うがー! Scala by Example, Scala Tutorial, Scala Reference とあたってもさっぱり分かりませんです。
思わずついったで

EclipseScalaプラグインの使い方がよーわからん。Example Scalaのサンプルどおりのソースがエラーになるのはなぜ? つかトップレベルで関数defするとエラーになるんですけど……。

と書いたら、kmizu さんから

@naruoga Scalaでは元々トップレベルでの関数定義を許していないんです。トップレベルに直接関数定義などを書くのは、「スクリプト」用に特別に許されているという感じです(具体的には scala Hoge.scala で直接実行する場合)。わかりにくいですが。 #scala

さらに、あースペシャルフォームなのか、でも文法的に食えるのに Eclipse でエラーになるのはいまいちじゃね? とこぼしたら、

@naruoga あ、すいません。書き方が悪かったです。スペシャルフォームというより、スクリプトモードの場合、コンパイラに食わせる前に処理系が object Main { def main(...){} }みたいなのを補うという意図でした。 #scala

というありがたーい教えが。
つまり def を直接定義できるのは scala コマンドで REPL とかスクリプト言語風に書きたいときだけであって、Eclipseデバッグする際には Scala としてちゃんとした形にしなきゃいかんと。わかんねーよ。そんなの。

んで、またネットでいろいろ調べて、結果、

abstract class IntTree
case object EmptyTree extends IntTree
case class Node(elem: Int, left: IntTree, right: IntTree) extends IntTree

object MyApplication {
  def contains(t: IntTree, v: Int): Boolean = t match {
  ... 
  }

  def insert(t: IntTree, v: Int): IntTree = t match {
  ...
  }
}

とすればコンパイルは通るようになります。が、当たり前ですがエントリポイントがないのでデバッグできません(^^;)
ので main も追加。

object MyApplication {
  ...
  def main(args: Array[String])
    var t: IntTree = EmptyTree
    t = insert(t,10)
    t = insert(t,8)
    t = insert(t,5)
    t = insert(t,12)
    t = insert(t,8)
    println(t)
  }
}

テストケースが網羅されてないとかはまあなしの方向で。

args 使ってないのに宣言してるのは試行錯誤の名残りです。いや、普通に main 宣言してもブレークしてくれなくて……。いや、最初は main がないって怒られたんだった。
いろいろ調べたけど object MyApplication extends Applicattion すればいいとか、それは古いノウハウだから止せとか書いてあって、あーもうコピペしたれってコピーしたらおっけーになったのでそのままです。

なおオイラは Java の人じゃないけどこの宣言って次の宣言と等価らしいすね。

class MyApplication {
  public static void main(String args[]){
  ...
}

ただし Scala には static はないので object にして一意性を確保するとかなんとか。これに限らず Scala でちょっと調べてると「Java で考えるとこーだろこの豚!」とかけっこう見かけて、世の中 Java やってない奴は Scala やるなという雰囲気がさらにやる気を削ぎます。

デバッグできねー!

っと、愚痴はよいとして。
これでやっと Eclipse 君は文句を言わず動いてくれるようになったのですが、ブレークポイントでブレークしてくれなくてまた悩みました。多分時間にして半日ぐらい(実時間では二日かな?)ここで悩んだんじゃないかしら。
これも Run -> Debug Configurations で Stop in main したら解決……というか Debug Perspective が最初は存在しなかった気がするんですが、いつのまにかできてた(苦笑)。
ソースをエディットする Perspective でブレークポイント指定したりしてもうまく行かない場合があるっぽいので、一度 Debug Perspective ができてしまえばしめたもの、のようです。なんで最初にできなかったかは謎。で、Stop in main すれば main で break するために Debug Perspective に切り替えるかい? って聞かれるので、よくわかんなかったらこいつを On にすればなんとかなるんだなって雑駁な理解で終了。いいんだ、俺 Eclipse で仕事する気ないから……。

んで、やっとデバッグ

えっと、このプログラムは「二分木で整数の集合を表現する」ものなんで、もう入れた値は弾かなきゃいけません。
だから前回のエントリで REPL テストしたとき t = Node(n, Node(m, ...), Node(l, ...)) (m < n < l) のときに insert(t, m) するとうまく行かないってのはこの判定時の処理が間違ってるんでしょうな。
ということで上の例の main で二個目の t = insert(t, 8) でブレークして追っていくと、一瞬で分かりました。

def insert(t: IntTree, v: Int): IntTree = t match {
  case EmptyTree => new Node(v, EmptyTree, EmptyTree)
  case Node(elem, left, right)
    => if (v == elem) t
       else if (v < elem)
         (if (contains(left,v))  left  // <-- ここと
          else                   Node(elem, insert(left, v), right))
       else
         (if (contains(right,v)) right // <-- ここ!
          else                   Node(elem, left, insert(right, v)))
}

サブツリーのなか覗いてるのにサブツリー自体帰したらダメに決まってるじゃないのさ。デバッガ使う前に気づけよ。
いや、デバッグの仕方を学ぶことも言語学習の一貫であって……。(言い訳)

じゃあ両方共 t 返せば……って、まてよ?
そもそも t に v が contains されてたらそのまま t 帰しちゃえばいいじゃん。左とか右とか見る前に。んで、contains されてなかったら v != elem は確定なので、あとは右か左に insert すればいいやんか。
最初に unification のつもりで case Node(v, _, _) ってのが浮かんだのがノイズになったなりよ。しくしく。
ということでこれでどうだ!

def insert(t: IntTree, v: Int): IntTree = t match {
  case EmptyTree => Node(v, EmptyTree, EmptyTree)
  case Node(elem, left, right)
    => if (contains(t, v)) t
       else if (v < elem) Node(elem, insert(left, v), right)
       else               Node(elem, left, insert(right, v))
}

ばっちり動作しました。ぱちぱち*1

……レベル低くてすみませんね。

通した感想

  • やっぱ Scala はあんまり萌えない。
    • 型しこしこ書かないといけないのはやっぱめんどくさい。
    • でも case class はやや面白いとは思う
    • パターンマッチは PrologErlang の用に使おうとするとダメっぽいので、もっと研究が必要かも。する気ないけど。
  • デバッグ機能のないインタプリタとか超使い道わかんない。電卓?
  • Eclispe は画面富豪の人向きなので Netbook で使うのは辛すぎる(笑)

あーそれはお前が低レベルなせいだという批判は甘んじて受けます。
さて次は Erlang かな?

*1:最初に case EmptyTree にうっかり new つけてたんですが、case class だから。頭の悪さを残すためにここ以外は残しておきます。