多忙な Java 開発者のための Scala ガイド
電卓を作る、第 3 回
Scala のパーサー・コンビネーターと case クラスを組み合わせる
コンテンツシリーズ
このコンテンツは全#シリーズのパート#です: 多忙な Java 開発者のための Scala ガイド
このコンテンツはシリーズの一部分です:多忙な Java 開発者のための Scala ガイド
このシリーズの続きに乞うご期待。
我慢強い読者の皆さん、このシリーズによく戻ってきてくださいました。Scala という言語とそのライブラリーを探る旅の続きとして、今月は電卓用の DSL を再度取り上げ、それを完成させます。DSL そのものは大したものではなく、今のところ基本的な四則演算しかサポートしない単純な電卓にすぎません。しかし目標は、拡張可能で柔軟性があり、しかも後に新しい機能をサポートするための機能強化作業を容易に行える電卓を作ることだということを忘れないでください。
前回までの電卓用 DSL の復習
復習すると、私達の DSL は現在まだ、少しばかりバラバラな状態にあります。まず、いくつかの case クラスから構成される AST があります。
リスト 1. バック・エンド (AST)
package com.tedneward.calcdsl { // ... private[calcdsl] abstract class Expr private[calcdsl] case class Variable(name : String) extends Expr private[calcdsl] case class Number(value : Double) extends Expr private[calcdsl] case class UnaryOp(operator : String, arg : Expr) extends Expr private[calcdsl] case class BinaryOp(operator : String, left : Expr, right : Expr) extends Expr }
これを元に、数式を単純化することで少し最適化を行うと、インタープリター風の動作を提供することができます。
リスト 2. バック・エンド (インタープリター)
package com.tedneward.calcdsl { // ... object Calc { def simplify(e: Expr): Expr = { // first simplify the subexpressions val simpSubs = e match { // Ask each side to simplify case BinaryOp(op, left, right) => BinaryOp(op, simplify(left), simplify(right)) // Ask the operand to simplify case UnaryOp(op, operand) => UnaryOp(op, simplify(operand)) // Anything else doesn't have complexity (no operands to simplify) case _ => e } // now simplify at the top, assuming the components are already simplified def simplifyTop(x: Expr) = x match { // Double negation returns the original value case UnaryOp("-", UnaryOp("-", x)) => x // Positive returns the original value case UnaryOp("+", x) => x // Multiplying x by 1 returns the original value case BinaryOp("*", x, Number(1)) => x // Multiplying 1 by x returns the original value case BinaryOp("*", Number(1), x) => x // Multiplying x by 0 returns zero case BinaryOp("*", x, Number(0)) => Number(0) // Multiplying 0 by x returns zero case BinaryOp("*", Number(0), x) => Number(0) // Dividing x by 1 returns the original value case BinaryOp("/", x, Number(1)) => x // Dividing x by x returns 1 case BinaryOp("/", x1, x2) if x1 == x2 => Number(1) // Adding x to 0 returns the original value case BinaryOp("+", x, Number(0)) => x // Adding 0 to x returns the original value case BinaryOp("+", Number(0), x) => x // Anything else cannot (yet) be simplified case e => e } simplifyTop(simpSubs) } def evaluate(e : Expr) : Double = { simplify(e) match { case Number(x) => x case UnaryOp("-", x) => -(evaluate(x)) case BinaryOp("+", x1, x2) => (evaluate(x1) + evaluate(x2)) case BinaryOp("-", x1, x2) => (evaluate(x1) - evaluate(x2)) case BinaryOp("*", x1, x2) => (evaluate(x1) * evaluate(x2)) case BinaryOp("/", x1, x2) => (evaluate(x1) / evaluate(x2)) } } } }
そしてまた、Scala のパーサー・コンビネーター・ライブラリーを使って作成された、単純な数式を構文解析するテキスト・パーサーがあります。
リスト 3. フロント・エンド
package com.tedneward.calcdsl { // ... object Calc { object ArithParser extends JavaTokenParsers { def expr: Parser[Any] = term ~ rep("+"~term | "-"~term) def term : Parser[Any] = factor ~ rep("*"~factor | "/"~factor) def factor : Parser[Any] = floatingPointNumber | "("~expr~")" def parse(text : String) = { parseAll(expr, text) } } // ... } }
しかしこのパーサーは、構文解析を行うと String と List のコレクションを生成します。これはパーサー・コンビネーターが現在 Parser[Any]
型を返すように作成されており、要するにパーサーが何を返すかはパーサーの自由に任されているためです (ここではご覧のとおり、String と List のコレクションを返しています)。
この DSL を役に立つものにするためには、構文解析が完了したときに実行エンジンがツリーを取得して、そのツリーに対して evaluate()
を実行できるように、パーサーは AST のオブジェクトを返す必要があります。そのためにはパーサー・コンビネーターの実装を変更し、構文解析中にさまざまなオブジェクトを生成させる必要があります。
文法を整理する
私はパーサーに対する最初の変更として、パーサーの実際の文法の 1 つをこれから説明するように変更することにしました。元のパーサーでは、式 (expr
) と項 (term
) に関する文法定義の中に rep()
コンビネーターがあるおかげで、「5 + 5 + 5」のような式に対応することができました。その一方で、私はパーサーを拡張することについて少し考えてみたところ、現状のパーサーのままでは、今後の演算で演算子の優先順位や結合規則に何らかの問題が起こる可能性があることに気付きました。しかしそうした問題は、括弧を使って演算の優先順位を明確にすることで防ぐことができます。そこで最初の変更は、すべての式の前後に ( ) を付けるように文法を変更することにします。
振り返ってみると、最初からこうしておくべきでした。一般に、後で制約を追加するよりも (制約が不必要または存在する意義がなくなった場合に) 制約を緩める方が容易なものですが、演算子の優先順位や結合規則に関する厄介な問題を解決することは、単に制約を追加するよりも遥かに困難です。演算子の優先順位や結合規則とはどのようなものなのかが確かではない方は、それに関する一般的な説明を探してみてください。この問題がどれほど複雑になるかの概要を知るためには、Java 言語そのものと Java 言語がサポートするさまざまな演算子 (Java 言語仕様に紹介されています) について考えるか、あるいは結合規則に関する難問 (Bloch と Gafter 著による『Java Puzzlers』に紹介されています) について考えてみてください。
そこで、1 つひとつ順々にステップを踏んで進めるために、まずは文法を再度テストしてみます。
リスト 4. 括弧を使う
package com.tedneward.calcdsl { // ... object Calc { // ... object OldAnyParser extends JavaTokenParsers { def expr: Parser[Any] = term ~ rep("+"~term | "-"~term) def term : Parser[Any] = factor ~ rep("*"~factor | "/"~factor) def factor : Parser[Any] = floatingPointNumber | "("~expr~")" def parse(text : String) = { parseAll(expr, text) } } object AnyParser extends JavaTokenParsers { def expr: Parser[Any] = (term~"+"~term) | (term~"-"~term) | term def term : Parser[Any] = (factor~"*"~factor) | (factor~"/"~factor) | factor def factor : Parser[Any] = "(" ~> expr <~ ")" | floatingPointNumber def parse(text : String) = { parseAll(expr, text) } } // ... } }
ここでは古いパーサーを OldAnyParser
とリネームして、比較できるようにそのまま残し、新しい文法には AnyParser
という名前を付けました。AnyParser では expr
を、term + term
、term - term
または単独の term
のいずれか、などと定義していることに注目してください。もう 1 つ重要な変更は factor
の定義の中にあります。新しい factor
では、~>
と <~
という別のコンビネーターを使っており、「(
」と「)
」という文字が使われている場合には実質的に「(
」と「)
」を破棄します。
これは暫定的なステップにすぎないため、すべての可能性をテストする一連のユニット・テストを作成するような手間はかけません。とは言え、この文法による構文解析の結果が期待どおりのものになることを確認したいので、ここでは少しばかり「ズル」をし、本物のテストとは言えないようなテストを作成します。
リスト 5. かなり「ズル」をしていますが、それでもパーサーをテストしています
package com.tedneward.calcdsl.test { class CalcTest { import org.junit._, Assert._ // ... @Test def parse = { import Calc._ val expressions = List( "5", "(5)", "5 + 5", "(5 + 5)", "5 + 5 + 5", "(5 + 5) + 5", "(5 + 5) + (5 + 5)", "(5 * 5) / (5 * 5)", "5 - 5", "5 - 5 - 5", "(5 - 5) - 5", "5 * 5 * 5", "5 / 5 / 5", "(5 / 5) / 5" ) for (x <- expressions) System.out.println(x + " = " + AnyParser.parse(x)) } } }
このコードは純粋に説明のために作成したものであることを忘れないでください (著者としては、あくまでも次のように助言します。「こんなコードを実際に本番用に作成してはいけませんが、ここでは本番用のコードを作成しているのではないため、「ズル」をしています。皆さんは決してこんなことをしてはいけません」)。このテストを実行すると、いくつかの結果がユニット・テストの結果ファイルの標準出力セクションに出力され、括弧を使わない式 (5 + 5 + 5
) が失敗する一方、括弧を使う式は成功することがわかります。素晴らしいことです。
この構文解析テストをコメントアウトすることを忘れないでください。あるいは、この構文解析テストを完全に削除してしまえばもっと良いでしょう。この構文解析テストは不正な改造を行ったものです。私達の誰もがよく知っているように、真のジェダイは知識と防御のためにソースを使うのであり、決して不正な改造のためには使わないものです (訳注: この文は著者が映画「スターウォーズ」のセリフ「A Jedi uses the Force for knowledge and defense, never for attack. (ジェダイは知識と防御のためにフォースを使うのだ。決して攻撃のためではない。)」にかけ、「a true Jedi only uses the Source for knowledge and defense, never for a hack.」としたものです)。
コンビネーターを整理する
今度は、さまざまなコンビネーターの定義を再度変更する必要があります (そうです、再度です)。前回の記事から、expr
、term
、factor
という各関数は基本的に BNF (Becker-Naur Form: バッカス・ナウア記法) の文法を使って記述されたステートメントであったことを思い出してください。ただしこれらの各ステートメントで返されるのは Any
でパラメーター化している汎用型の Parser であることに注目してください (Any は Scala の型システムのなかでも究極のスーパータイプであり、基本的にその名前どおりの役割を果たし、どんなものでも含みうる潜在的な型または参照を示します)。これはつまり、コンビネーターは自由に好きなものを返せるということです。既に見たように、パーサーはデフォルトで、String または List を返します。(あまり確信がない方は、先ほどの不正改造したテストを実行した結果を見てもわかるはずです)。
case クラスの AST の階層構造のインスタンス (Expr
オブジェクト) を生成するようにパーサーを変更するためには、コンビネーターの戻り型を Parser[Expr]
に変更する必要があります。この変更を単独で行うと、コンパイルに失敗します。これは 3 つのコンビネーターは String を取り込む方法は知っていても構文解析した結果から Expr
オブジェクトを生成する方法を知らないためです。そこで、匿名関数をパラメーターに取るさらにもう 1 つのコンビネーター ^^
を使って、その匿名関数に構文解析の結果をパラメーターとして渡します。
Java 開発者の多くは、この第 2 の構文解析処理を組み込む作業をすぐに行えることでしょう。では、この処理の実際を見てみましょう。
リスト 6. 本番レベルのコンビネーター
package com.tedneward.calcdsl { // ... object Calc { object ExprParser extends JavaTokenParsers { def expr: Parser[Expr] = (term ~ "+" ~ term) ^^ { case lhs~plus~rhs => BinaryOp("+", lhs, rhs) } | (term ~ "-" ~ term) ^^ { case lhs~minus~rhs => BinaryOp("-", lhs, rhs) } | term def term: Parser[Expr] = (factor ~ "*" ~ factor) ^^ { case lhs~times~rhs => BinaryOp("*", lhs, rhs) } | (factor ~ "/" ~ factor) ^^ { case lhs~div~rhs => BinaryOp("/", lhs, rhs) } | factor def factor : Parser[Expr] = "(" ~> expr <~ ")" | floatingPointNumber ^^ {x => Number(x.toFloat) } def parse(text : String) = parseAll(expr, text) } def parse(text : String) = ExprParser.parse(text).get // ... } // ... }
^^
コンビネーターは匿名関数を利用し、この関数に構文解析の結果 (例えば入力が 5 + 5
の場合、結果は ((5~+)~5)
になるはずです) を渡すと、その結果が分析されてオブジェクト (この場合は該当するタイプの BinaryObject
) が生成されます。この場合にも、パターン・マッチングの強力さに注目してください。この場合は、式の左側部分を lhs
インスタンスに、+
部分を (使われていない) plus
インスタンスに、そして式の右側を rhs
に、それぞれバインディングし、即座に lhs
と rhs
の両方を使ってそれぞれ BinaryOp
コンストラクターの左側と右側に入力しています。
この状態でコードをユニット・テスト・スイートに対して実行すると (不正な改造を行った部分を忘れずにコメント・アウトしてください)、すべて好ましい結果が得られます。これまでに試したさまざまな式は、もう失敗することはありません。これはパーサーが Expr
から派生した形のオブジェクトを生成するためです。とは言え、このパーサーをさらに試してみないと無責任です。そこで、(先ほどのパーサーでちょっと「ズル」をしたテストを含めて) もう少しテストを追加してみましょう。
リスト 7. パーサーをテストする (今回は本物のテストです)
package com.tedneward.calcdsl.test { class CalcTest { import org.junit._, Assert._ // ... @Test def parseAnExpr1 = assertEquals( Number(5), Calc.parse("5") ) @Test def parseAnExpr2 = assertEquals( Number(5), Calc.parse("(5)") ) @Test def parseAnExpr3 = assertEquals( BinaryOp("+", Number(5), Number(5)), Calc.parse("5 + 5") ) @Test def parseAnExpr4 = assertEquals( BinaryOp("+", Number(5), Number(5)), Calc.parse("(5 + 5)") ) @Test def parseAnExpr5 = assertEquals( BinaryOp("+", BinaryOp("+", Number(5), Number(5)), Number(5)), Calc.parse("(5 + 5) + 5") ) @Test def parseAnExpr6 = assertEquals( BinaryOp("+", BinaryOp("+", Number(5), Number(5)), BinaryOp("+", Number(5), Number(5))), Calc.parse("(5 + 5) + (5 + 5)") ) // other tests elided for brevity } }
読者の皆さんも、私が稀なケースを見逃していないことを確認するために、ぜひいくつかのテストを追加してみてください。(私に言わせれば、インターネットを相手にする際には、誰かとペアを組んでプログラミングするほど効果的なものはありません。)
結び付けられていない部分を結合する
私達の望むとおりにパーサーが動作する (つまり AST を生成する) ようになったので、あとはパーサーによる解析結果を AST オブジェクトの評価と結びつけ、どんなものが生まれるかを見ればよいだけです。そのために必要なことは、ほとんど拍子抜けしてしまいますが、リスト 8 のコードを Calc
に追加するだけです。
リスト 8. これでいよいよ完成です
package com.tedneward.calcdsl { // ... object Calc { // ... def evaluate(text : String) : Double = evaluate(parse(text)) } }
そして簡単なテストを追加し、evaluate("1+1")
によって 2.0 が返されることを確認します。
リスト 9. 単に 1 + 1 が 2 であることを確認するために、これだけのことが必要です
package com.tedneward.calcdsl.test { class CalcTest { import org.junit._, Assert._ // ... @Test def add1 = assertEquals(Calc.evaluae("1 + 1"), 2.0) } }
そしてこれを実行してみます。冗談抜きで、実際に実行してみると気分が良いと思いませんか。
言語を拡張する
これまで行っていたような手間をわざわざかけなくても、同じ電卓用の DSL を単純な Java コードでも作成することができる (例えば AST 全体を構築せずに各フラグメントを単に再帰的に評価するなど) ということを考えると、これまでの作業は、言語やツールで問題を見つけて解決しようとする、よくある例と言えるかもしれません。しかしここで説明した方法で言語を作成する真の強みは、後から拡張やスケールアップを行って機能を追加する際に発揮されるのです。
例えば、この言語に新しい演算子 (^) を追加しましょう。^
は指数演算を行います。つまり 2 ^ 2 は 2 の 2 乗、つまり 4 です。これを言語に追加するためには、いくつか単純なステップが必要です。
まず、AST を変更する必要があるかどうかを考える必要があります。この場合、指数演算はバイナリー演算子の別形式であるため、既存の BinaryOp
ケース・クラスが使えます。つまり AST の変更は必要ありません。
次に、BinaryOp("^", x, y)
で適切な指数演算をするように evaluate
関数を変更する必要があります。それには、実際の指数計算を扱うネストされた関数 (外から見える必要がないのでネストされています) を追加してから、パターン・マッチングを行うために必要な行を追加すればよいだけです。すると次のようになります。
リスト 10. 指数演算への対応
package com.tedneward.calcdsl { // ... object Calc { // ... def evaluate(e : Expr) : Double = { def exponentiate(base : Double, exponent : Double) : Double = if (exponent == 0) 1.0 else base * exponentiate(base, exponent - 1) simplify(e) match { case Number(x) => x case UnaryOp("-", x) => -(evaluate(x)) case BinaryOp("+", x1, x2) => (evaluate(x1) + evaluate(x2)) case BinaryOp("-", x1, x2) => (evaluate(x1) - evaluate(x2)) case BinaryOp("*", x1, x2) => (evaluate(x1) * evaluate(x2)) case BinaryOp("/", x1, x2) => (evaluate(x1) / evaluate(x2)) case BinaryOp("^", x1, x2) => exponentiate(evaluate(x1), evaluate(x2)) } } } }
ここで、システムへの指数演算の追加が実質的に 6 行のコードですんでおり、Calc
クラスの他の部分には表面的にまったく変更がないことに注目してください。これは素晴らしいカプセル化です。
(私は動作しうる最も単純な指数関数を作成しようとしたため、非常に重大な (ただし意図的な) バグを含んだバージョンを故意に作成しました。このようにした理由は、実装ではなく言語に注目させるためです。とは言え、そのバグを見つけ、それを明らかにするユニット・テストを作成し、修正版を提供してくださる最初の読者は栄誉に値するものであり、その読者には謝意を表したいと思います。)
ただしこれをパーサーに追加する前に、指数演算の部分が適切に動作することを確認するテストをいくつか作成し、このコードを実際に試して (アジャイル・プログラミングの仲間の間では「keepin' it real (実際に動くようにする)」と言います) みましょう。
リスト 11. 平方演算の確認
package com.tedneward.calcdsl.test { class CalcTest { // ... @Test def evaluateSimpleExp = { val expr = BinaryOp("^", Number(4), Number(2)) val results = Calc.evaluate(expr) // (4 ^ 2) => 16 assertEquals(16.0, results) } @Test def evaluateComplexExp = { val expr = BinaryOp("^", BinaryOp("*", Number(2), Number(2)), BinaryOp("/", Number(4), Number(2))) val results = Calc.evaluate(expr) // ((2 * 2) ^ (4 / 2)) => (4 ^ 2) => 16 assertEquals(16.0, results) } } }
このコードを実行してみると、(先ほど触れたバグはありますが) 指数演算が動作することが検証されます。これで、作業の半分が終わりました。
必要な最後の変更は、新しい指数演算子を受け付けるように文法を変更することです。指数演算はこれまでの乗算と除算という前例と同じレベルなので、term
コンビネーターの中に指数演算子を置くようにする方法が最も簡単です。
リスト 12. これで終了、本当に終了です
package com.tedneward.calcdsl { // ... object Calc { // ... object ExprParser extends JavaTokenParsers { def expr: Parser[Expr] = (term ~ "+" ~ term) ^^ { case lhs~plus~rhs => BinaryOp("+", lhs, rhs) } | (term ~ "-" ~ term) ^^ { case lhs~minus~rhs => BinaryOp("-", lhs, rhs) } | term def term: Parser[Expr] = (factor ~ "*" ~ factor) ^^ { case lhs~times~rhs => BinaryOp("*", lhs, rhs) } | (factor ~ "/" ~ factor) ^^ { case lhs~div~rhs => BinaryOp("/", lhs, rhs) } | (factor ~ "^" ~ factor) ^^ { case lhs~exp~rhs => BinaryOp("^", lhs, rhs) } | factor def factor : Parser[Expr] = "(" ~> expr <~ ")" | floatingPointNumber ^^ {x => Number(x.toFloat) } def parse(text : String) = parseAll(expr, text) } // ... } }
もちろん、パーサーを実行するためのテストも、いくつか必要です。
リスト 13. パーサーでの平方演算の確認
package com.tedneward.calcdsl.test { class CalcTest { // ... @Test def parseAnExpr17 = assertEquals( BinaryOp("^", Number(2), Number(2)), Calc.parse("2 ^ 2") ) @Test def parseAnExpr18 = assertEquals( BinaryOp("^", Number(2), Number(2)), Calc.parse("(2 ^ 2)") ) @Test def parseAnExpr19 = assertEquals( BinaryOp("^", Number(2), BinaryOp("+", Number(1), Number(1))), Calc.parse("2 ^ (1 + 1)") ) @Test def parseAnExpr20 = assertEquals( BinaryOp("^", Number(2), Number(2)), Calc.parse("2 ^ (2)") ) } }
これを実行するとテストにパスし、あとは最終的なテストとして、すべてが適切に結合されているかどうかを調べればよいだけです。
図 14. String から平方演算へ
package com.tedneward.calcdsl.test { class CalcTest { // ... @Test def square1 = assertEquals(Calc.evaluate("2 ^ 2"), 4.0) } }
大成功です。
まとめ
当然のことですが、これほど単純な言語の場合には、メリットと比べると作業が多すぎるかもしれません。皆さんがこの言語の個々の部分 (AST、パーサー、単純化エンジンなど) のテストに関してどう感じるかはともかく、もっと単純なインタープリター・ベースのオブジェクトとして言語を単純に作成した方が (さらには、式を AST に変換して AST から式を評価するよりも即時動作で式を計算した方が)、ずっと近道だったかも (そしてこの作業をどれほど頻繁に行うかの頻度によっては、もっと速く作業を行えたかも) しれません。
しかし、1 つの演算子をシステムに追加することがどれほど簡単だったか、またこの言語の実装がこのように設計されているおかげで実際には非常に容易に拡張を行うことができ、さまざまなコードの部分に触れる必要がなかったことを考えてみてください。実際、この方法に特有の柔軟性を示す機能強化は、例えば以下に示すようなものなど他にも数多く考えられます。
Doubles
ではなく、(もっと大規模な計算やもっと正確な計算を行えるように) java.math パッケージのBigDecimals
またはBigIntegers
を使うように変更する。- 小数のサポートを言語に追加する (現在のパーサーではサポートされていません)。
- 記号ではなく単語 (「sin」、「cos」、「tan」など) を使う新しい演算子を追加する。
- さらには変数による表記 ("x = y + 12" など) を追加し、そうした各変数の開始値を含む
evaluate()
関数のパラメーターとしてMap
を受け付ける。
もっと重要なこととして、この DSL はファサードとしての Calc
クラスの背後に完全に隠れており、Java コードから呼び出すことも Scala から呼び出すことも非常に簡単です。そのため、言語の第一選択肢として全面的に Scala を採用する用意はないプロジェクトであっても、システムの一部 (関数型とオブジェクト型が融合した言語にとって最適な部分) を Scala で作成し、単純に Java 開発者が自由に利用できるようにすることもできます。
これで今回の説明は終わりです。次の一連の記事では Scala の言語機能を再度検証します (例えばパーサー・コンビネーターが Generics を利用しているため Generics を再検証する、など)。Scala には、まだ学ぶべきことは大量にあります。しかしここまで読み進んできたことで、これまで Java コードでは非常に困難だった問題が Scala を使えば解決できることが、少しでも明らかになったようであれば幸いです。では、次回の記事をご期待ください。
ダウンロード可能なリソース
関連トピック
- 「多忙な Java 開発者のための Scala ガイド: オブジェクト指向のための関数型プログラミング」(Ted Neward 著、developerWorks、2008年1月) はこのシリーズの第 1 回として、何よりも Scala の概要と、並行性に対して Scala が持つ関数型の手法を解説しています。このシリーズの他の記事は次のとおりです。
- 「クラスの動作」(2008年2月) は Scala のクラスの構文とセマンティクスについて詳細に解説しています。
- 「Don't get thrown for a loop!」(2008年3月) は Scala の制御の構造の内部を深く掘り下げています。
- 「trait と振る舞い」(2008年4月) は Scala バージョンの Java インターフェースの活用方法を解説しています。
- 「実装継承」(2008年5月) は Scala 流のポリモーフィズムを紹介しています。
- 「コレクション型」(2008年6月) は「タプルと配列、そしてリスト」のすべてを解説しています。
- 「パッケージとアクセス修飾子」(2008年7月) は Scala のパッケージ機能とアクセス修飾子機能、そして apply の仕組みを解説しています。
- 「電卓を作る、第 1 回」(2008年8月) はこの記事で取り上げているレッスンの第 1 回です。
- 「電卓を作る、第 2 回」(2008年10月) はこの記事で取り上げているレッスンの第 2 回であり、パーサー・コンビネーターの使い方を説明しています。
- 「Functional programming in the Java language」(Abhijit Belapurkar 著、developerWorks、2004年7月) は、Java 開発者の視点から関数型プログラミングの利点と使い方を説明しています。
- 「Scala by Example」(Martin Odersky 著、2007年12月) は簡潔に、コードを中心に Scala を紹介しています (PDF)。
- 『Programming in Scala』(Martin Odersky、Lex Spoon、Bill Venners の共著、2007年12月、Artima 刊) は 1 冊の本として Scala を紹介した最初の資料です。
- 『Java Puzzlers: Traps, Pitfalls, and Corner Cases』 (2005年7月 Addison-Wesley Professional 刊) は Java 言語の風変わりな点を深く考えさせる楽しいプログラミング・パズルをとおして明らかにしています。
- developerWorks の Java technology ゾーンには、Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。
- Scala をダウンロードし、このシリーズと共に Scala を学んでください。
- SUnit は Scala の標準的なディストリビューションの一部であり、scala.testing パッケージの中にあります。