Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Essential Scala 第4章 トレイトによるデータモデリング

11 views

Published on

2018年8月1日にアカウンティング・サース・ジャパン株式会社で開催した「Essential Scala 輪読会 #3」のスライドです。今回の内容は「第4章 トレイトによるデータモデリング」です。

Published in: Technology
  • Be the first to comment

  • Be the first to like this

Essential Scala 第4章 トレイトによるデータモデリング

  1. 1. Essential Scala 第4章 トレイトによるデータモデリング 2018.8.1 Takuya Tsuchida (@takuya0301)
  2. 2. 第4章 トレイトによるデータモデリング 4.1 トレイト 4.2 あれかこれかで他にはない:シールドトレイト 4.3 トレイトによるデータモデリング 4.4 直和型パターン 4.5 データで作業する 4.6 再帰的データ 4.7 大きな例 4.8 まとめ 2
  3. 3. 第4章 トレイトによるデータモデリング 前章でクラスの奥深さを見ました。クラスは類似した属性を持つオブジェクトを抽象化す る方法を提供し、クラスに属するどんなオブジェクトでも動作するコードを書けるようにな りました。 本章ではクラスを抽象化する方法を探検し、異なるクラスのオブジェクトでも動作する コードを書きます。これをトレイトと呼ばれるメカニズムで実現します。 本章は視点の変更を示します。前章では Scala コードを構成する技術的側面を重視し てきました。本章のはじめでもトレイトの技術的側面を重視します。その視点は、思考を 表現する媒体として Scala を使用することに変化していきます。 代数的データ型と呼ばれるデータの記述をコードに機械的に変換する方法を見ていきま す。構造的再帰を使用することで、代数的データ型を変換するコードを機械的に記述で きます。 3
  4. 4. 4.1 トレイト トレイトはクラス作成のためのテンプレートで、クラスがオブジェクト作成のテンプレートで あるのと同様です。トレイトは2つ以上のクラスを同じと見なす表現で、いずれも同じ操作 を実装します。言い換えると、トレイトは共通の基底型を共有する複数のクラスを表現し ます。 4
  5. 5. ノート:トレイト対 Java インターフェイス トレイトはデフォルトメソッドを伴う Java 8 のインターフェイスにとても似ている。Java 8 を使用したことがないのであれば、インターフェイスと抽象クラスを掛け合わせたものが トレイトであると考えていい。 5
  6. 6. 4.1.1 トレイトの例 トレイトの例からはじめます。Web サイトの訪問者をモデリングすることを想像します。 訪問者には2つの型、サイトに登録済みと匿名があります。 case class Anonymous( id: String, createdAt: Date = new Date() ) case class User( id: String, email: String, createdAt: Date = new Date() )
  7. 7. 4.1.1 トレイトの例 そこには明白な重複がありますが、同じ定義を2回書かない方がいいでしょう。より重要 ことは、2種類の訪問者に共通する型をつくることです。共通の型を持たせることができ れば、どの種類の訪問者でも動作するメソッドを書くことができます。これをトレイトで実 現します。 trait Visitor { def id: String // 各ユーザーに付与されるユニークな ID def createdAt: Date // このユーザーがサイトにはじめて訪れた日付 // 訪問者がどのくらいの時間そこにいたか? def age: Long = new Date().getTime - createdAt.getTime }
  8. 8. 4.1.1 トレイトの例 case class Anonymous( id: String, createdAt: Date = new Date() ) extends Visitor case class User( id: String, email: String, createdAt: Date = new Date() ) extends Visitor
  9. 9. 4.1.1 トレイトの例 下記の2つについて変更しています。 ● Visitor トレイトを定義する ● extends キーワードを使用し Visitor トレイトの派生型として Anonymous と User を宣言する Visitor トレイトは、どんな派生型でも必ず実装しなければならないインターフェイスを表 現します。派生型は id と呼ばれる文字列と日付の createdAt を必ず実装します。また、 どんな Visitor の派生型でも Visitor に定義されている age メソッドを自動的に持ちま す。
  10. 10. 4.1.1 トレイトの例 Visitor トレイトを定義することによって、どんな訪問者の派生型についても動作するメ ソッドを書くことができます。 def older(v1: Visitor, v2: Visitor): Boolean = v1.createdAt.before(v2.createdAt) older(Anonymous("1"), User("2", "test@example.com")) // res4: Boolean = true older メソッドは Visitor の派生型である Anonymous でも User でも呼び出すことがで きます。
  11. 11. ノート:トレイト文法 トレイトは下記のように宣言できる。 trait TraitName { declarationOrExpression ... } トレイトの派生型であるクラスやケースクラスは下記のように宣言できる。 class Name(...) extends TraitName { ... } case class Name(...) extends TraitName { ... } 14
  12. 12. 4.1.2 トレイトとクラスの比較 クラスのように、トレイトはフィールド定義とメソッド定義の名付けられた集合です。しか し、下記の点でクラスとは違います。 トレイトはコンストラクターを持てません。トレイトからは直接オブジェクトを生成できませ ん。その代わりにクラスを生成するためにトレイトを使用し、そのクラスからオブジェクト を生成します。 トレイトは名前と型を持ち実装を持たない抽象メソッドを定義できます。これを Visitor ト レイトで見ました。トレイトを拡張したクラスを生成するときには実装を指定する必要があ りますが、それまでは定義を抽象なままにしておいてかまいません。
  13. 13. 4.1.2 トレイトとクラスの比較 抽象的な定義を探すために Visitor トレイトに立ち返ってみましょう。 trait Visitor { def id: String // 各ユーザーに付与されるユニークな ID def createdAt: Date // このユーザーがサイトにはじめて訪れた日付 // 訪問者がどのくらいの時間そこにいたか? def age: Long = new Date().getTime - createdAt.getTime } Visitor は2つの抽象メソッドを規定しています。メソッドは実装を持ちませんが、派生クラ スで実装されなければなりません。その対象として id と createdAt があります。また、ひ とつの抽象メソッドを項として定義に含む、具象メソッドである age を定義しています。
  14. 14. 4.1.2 トレイトとクラスの比較 Visitor は Anonymous と User という2つのクラスのためのビルディングブロックとして 使用されています。各クラスは Visitor を拡張しており、それはそのフィールドとメソッド のすべてを継承していることを意味します。 Anonymous("anon1") // res14: Anonymous = Anonymous(anon1) res14.createdAt // res15: java.util.Date = Mon Mar 24 15:11:45 GMT 2014 res14.age // res16: Long = 8871
  15. 15. 4.1.2 トレイトとクラスの比較 id と createdAt は抽象なので、それらは派生クラスで定義される必要があります。例の クラスでは def ではなく val として実装しています。Scala ではこれが認められており、 val を一般化したものが def であると見なされます * 。トレイトの中で val を定義せずに def を使用することはいいことです。具体的な実装は適切に def か val を使用して実装 しましょう。 * オブジェクトリテラルについての課題で見た統一形式アクセスの原則のことです。
  16. 16. 4.1.3 キーポイント:トレイト トレイトは、オブジェクトを抽象化する方法としてのクラスのように、類似した属性を持つ クラスを抽象化する方法です。 トレイトと、トレイトを継承したクラス(普通はケースクラス)を宣言することで使用します。 trait TraitName { declarationOrExpression ... } case class Name(...) extends TraitName { ... }
  17. 17. 4.2 あれかこれかで他にはない:シールドトレイト 多くの場合に、トレイトを拡張することが考えうるすべてのクラスを列挙できます。例え ば、Web サイトの訪問者を Anonymous かログイン済みの User としてモデリングしま した。それら2つのケースは可能なもののすべてを含むので、一方のケースの反対は他 方のケースになります。これはシールドトレイトでモデリングでき、コンパイラーが追加の 検査を提供してくれます。
  18. 18. 4.2 あれかこれかで他にはない:シールドトレイト シールドトレイトはトレイト宣言に sealed と記述するだけで作成できます。 sealed trait Visitor { def id: String def createdAt: Date def age: Long = new Date().getTime() - createdAt.getTime() } case class User(id: String, email: String, createdAt: Date = new Date()) extends Visitor case class Anonymous(id: String, createdAt: Date = new Date()) extends Visitor
  19. 19. 4.2 あれかこれかで他にはない:シールドトレイト シールドとしてトレイトをマークしたときは、同じファイルですべての派生型を定義しなけ ればなりません。ひとたびトレイトがシールドになると、コンパイラーは派生型の完全な 集合を把握し、パターンマッチ式で不足しているケースがあると警告してくれます。 def missingCase(v: Visitor) = v match { case User(_, _, _) => "Got a user" } // <console>:15: warning: match may not be exhaustive. // It would fail on the following input: Anonymous(_, _) // def missingCase(v: Visitor) = v match { // ^ // missingCase: (v: Visitor)String
  20. 20. 4.2 あれかこれかで他にはない:シールドトレイト シールドトレイトの派生型は、それが定義されているファイル外で拡張できてしまいま す。派生型をファイル内での拡張が可能な sealed か final として宣言することで防ぐこ とができます。 sealed trait Visitor { /* ... */ } final case class User(/* ... */) extends Visitor final case class Anonymous(/* ... */) extends Visitor
  21. 21. ノート:シールドトレイト文法 トレイトにおけるすべての派生型が既知であればトレイトを封印すべきである。 sealed trait TraitName { ... } 派生型を拡張することがなければ、派生型を final にすることを検討すべきである。 final case class Name(...) extends TraitName { ... } なお、派生型はシールドトレイトと同じファイルで定義されなければならない。 26
  22. 22. 4.2.1 キーポイント:シールドトレイト シールドトレイトとファイナル(ケース)クラスは型の拡張性を制御できるようにします。大 多数はシールドトレイトとファイナルケースクラスのパターンとして使用されます。 sealed trait TraitName { ... } final case class Name(...) extends TraitName このパターンの主な利点は、コンパイラーがパターンマッチで不足している型を警告して くれることと、シールドトレイトを拡張する場所を制御することで派生型の振る舞いを保証 できることです。
  23. 23. 4.3 トレイトによるデータモデリング 本節では言語機能からプログラミングパターンに焦点を移していきます。データモデリン グについて見ていきながら、論理和と論理積の見地から様々なデータモデルをScala で 表現するプロセスを学びます。オブジェクト指向プログラミングの専門用語である is-a 関 係と has-a 関係を表現し、関数プログラミングの専門用語である、代数的データ型と呼 ばれる直和型と直積型を学びます。 本節でのゴールは、どうデータモデルを Scala コードに変換するかを見るということで す。次節では代数的データ型を使用したコードのパターンを見ます。
  24. 24. 4.3.1 直積型パターン 最初のパターンは、他のデータを含むデータをモデリングするものです。「A は B と C を 持つ」と説明できます。 これはケースクラスを使用して記述します。
  25. 25. ノート:直積型パターン A が B 型の b と C 型の c を持つ場合、このように記述する。 case class A(b: B, c: C) or trait A { def b: B def c: C } 30
  26. 26. 4.4 直和型パターン * 次のパターンは、2つ以上の異なったケースになるデータをモデリングするものです。「A は B か C である」と説明できます。 これはシールドトレイトとケースクラスのパターンで記述します。 * 訳注:節番号が 4.3.2 になる内容です。おそらく誤植と考えられます。
  27. 27. ノート:直和型パターン A が B か C である場合、このように記述する。 sealed trait A final case class B() extends A final case class C() extends A 32
  28. 28. 4.4.1 代数的データ型 代数的データ型は、前述の2つのパターンを使用したデータになります。関数プログラミ ングの文脈では、has-a and パターンは直積型であり、is-a or パターンは直和型です。
  29. 29. 4.4.2 不足しているパターン is-a / has-a と and / or という2軸の関係性を見てきました。この定義を表にすると4つの セルについて2つのパターンしか見ていません。 2つの不足しているパターンは何でしょうか? And Or Is-a 直和型 Has-a 直積型
  30. 30. 4.4.2 不足しているパターン is-a and パターンは「A は B かつ C である」を意味します。このパターンはある点にお いて直和型の逆になり、下記のように実装できます。 trait B trait C trait A extends B with C
  31. 31. 4.4.2 不足しているパターン トレイトはたくさんのトレイトから拡張することができます。しかし、本稿ではこのパターン を使用しません。異なるインターフェイスで構成されるデータを表現する場合、あとから 学習する型クラスを使用します。 道理に合っている場合には、is-a and パターンを使用することもあります。 ● モジュール性のためにケークパターン * を使用する場合 ● いくつかのクラスを横断して実装を共有するときに、メイントレイトがデフォルト実装 を持つことが妥当でない場合 * http://jonasboner.com/2008/10/06/real-world-scala-dependency-injection-di/
  32. 32. 4.4.2 不足しているパターン has-a or パターンは「A は B か C を持つ」を意味します。これを実装するには2つの方 法があります。 「A は D を持ち、D は B か C である」と言えます。これを実装するために2つのパターン を機械的に適用することができます。 trait A { def d: D } sealed trait D final case class B() extends D final case class C() extends D
  33. 33. 4.4.2 不足しているパターン あるいは、「A は D か E である、D は B を持ち、E は C を持つ」と言えます。これも直 接的にコードに変換できます。 sealed trait A final case class D(b: B) extends A final case class E(c: C) extends A
  34. 34. 4.4.3 キーポイント:トレイトによるデータモデリング 直積型による has-a and パターンと直和型による is-a or パターンで、データを機械的 に Scala コードを変換するのを見ました。このタイプのデータは代数的データ型として知 られています。それらのパターンを理解することは理想的な Scala コードを書く上でとて も重要です。
  35. 35. 4.5 データで作業する 本節では代数的データ型を使用したパターン、構造的再帰を見ていきます。 構造的再帰は代数的データ型を組み立てるプロセスのちょうど反対です。直和型パター ンと直積型パターンは、組み合わせて大きなデータをつくる方法を教えてくれます。構造 的再帰は本質的には小さな部品に分割するプロセスです。 代数的データ型を構築するためにちょうど2つのパターンがありましたが、構造的再帰を 用いて分解するためにも2つのパターンがあります。実際にはそれぞれのパターンにつ いて2つの変種があり、ひとつは典型的なオブジェクト指向スタイルでポリモーフィズムを 使用したもので、もうひとつは典型的な関数スタイルでパターンマッチを使用したもので す。どちらの使用を選択すべきかのいくつかのルールで本節を締め括ります。
  36. 36. 4.5.1 ポリモーフィズムを使用した構造的再帰 多相ディスパッチもしくはポリモーフィズムはオブジェクト指向技術の基礎をなします。ト レイトでメソッドを定義し、トレイトを拡張したクラスで異なる実装を持つ場合、具象インス タンスで実装されたメソッドが呼び出されます。
  37. 37. 4.5.1 ポリモーフィズムを使用した構造的再帰 直和型を使用したシンプルな定義から始めます。 sealed trait A { def foo: String } final case class B() extends A { def foo: String = "It's B!" } final case class C() extends A { def foo: String = "It's C!" }
  38. 38. 4.5.1 ポリモーフィズムを使用した構造的再帰 val anA: A = B() // anA: A = B() anA.foo // res1: String = It's B! val anA: A = C() // anA: A = C() anA.foo // res2: String = It's C!
  39. 39. 4.5.1 ポリモーフィズムを使用した構造的再帰 トレイトで実装を定義することもでき、派生クラスでは override キーワードを使用して実 装を変更します。 sealed trait A { def foo: String = "It's A!" } final case class B() extends A { override def foo: String = "It's B!" } final case class C() extends A { override def foo: String = "It's C!" }
  40. 40. 4.5.1 ポリモーフィズムを使用した構造的再帰 val anA: A = B() // anA: A = B() anA.foo // res1: String = It's B! トレイトでデフォルト実装を提供する場合、すべての派生型で妥当な実装であることを確 実にすべきということを覚えておいてください。
  41. 41. ノート:直積型ポリモーフィズムパターン A が B 型の b と C 型の c を持ち、F 型を返すメソッド f を記述したい場合、シンプルに いつもの方法でメソッドを記述する。 case class A(b: B, c: C) { def f: F = ??? } メソッドの本体で、F 型の結果を構築するために、 b と c とメソッド引数を必ず使用しな ければならない。 48
  42. 42. ノート:直和型ポリモーフィズムパターン A が B か C であり、F 型を返すメソッド f を記述したい場合、A で f を抽象メソッドとして 定義し、B と C で具象実装を提供する。 sealed trait A { def f: F } final case class B() extends A { def f: F = ??? } final case class C() extends A { def f: F = ??? } 49
  43. 43. 4.5.2 パターンマッチを使用した構造的再帰 パターンマッチによる構造的再帰はポリモーフィズムと同じ線に沿って進みます。それぞ れの派生型についてのケースをシンプルに持ち、パターンマッチにおける各ケースにお いて興味のあるフィールドを抽出できます。
  44. 44. ノート:直積型パターンマッチパターン A が B 型の b と C 型の c を持ち、A を受け入れ F を返すメソッド f を記述したい場合、 下記のように記述する。 def f(a: A): F = a match { case A(b, c) => ??? } メソッドの本体で、F 型の結果を構築するために、 b と c を使用する。 51
  45. 45. ノート:直和型パターンマッチパターン A が B か C であり、A を受け入れ F を返すメソッド f を記述したい場合、B と C をパ ターンマッチのケースとして定義する。 def f(a: A): F = a match { case B() => ??? case C() => ??? } 52
  46. 46. 4.5.3 完全な例:共通 sealed trait Food final case object Antelope extends Food final case object TigerFood extends Food final case object Licorice extends Food final case class CatFood(food: String) extends Food
  47. 47. 4.5.3 完全な例:ポリモーフィズム sealed trait Feline { def dinner: Food } final case class Lion() extends Feline { def dinner: Food = Antelope } final case class Tiger() extends Feline { def dinner: Food = TigerFood } final case class Panther() extends Feline { def dinner: Food = Licorice } final case class Cat(favouriteFood: String) extends Feline { def dinner: Food = CatFood(favouriteFood) }
  48. 48. 4.5.3 完全な例:パターンマッチ(基底トレイト) sealed trait Feline { def dinner: Food = this match { case Lion() => Antelope case Tiger() => TigerFood case Panther() => Licorice case Cat(favouriteFood) => CatFood(favouriteFood) } } final case class Lion() extends Feline final case class Tiger() extends Feline final case class Panther() extends Feline final case class Cat(favouriteFood: String) extends Feline
  49. 49. 4.5.3 完全な例:パターンマッチ(別オブジェクト) sealed trait Feline final case class Lion() extends Feline final case class Tiger() extends Feline final case class Panther() extends Feline final case class Cat(favouriteFood: String) extends Feline object Diner { def dinner(feline: Feline): Food = feline match { case Lion() => Antelope case Tiger() => TigerFood case Panther() => Licorice case Cat(food) => CatFood(food) } }
  50. 50. 4.5.4 使用するパターンの選び方 構造的再帰を実装するのに3つの方法があります。 1. ポリモーフィズム 2. 基底トレイトでのパターンマッチ 3. 外部オブジェクトでのパターンマッチ 使用するパターンは下記のルールを参考に選んでみてください。 ● 実装がクラス内のフィールドやメソッドだけに依存している → 1 or 2(一般的に重複するコードが少ない2が選ばれる) ● 実装がクラス外のデータに依存している → 3 ● 複数の実装が必要になる → 3
  51. 51. 4.5.5 「オブジェクト指向」対「関数的拡張性」 典型的な関数プログラミングスタイルはパターンマッチを使用し、典型的なオブジェクト 指向スタイルはポリモーフィズムを使用します。関数プログラミングスタイルの利点はコ ンパイラーがより手助けしてくれることです。 オブジェクト指向スタイルは、トレイトを拡張することで新しいデータを容易に追加できま すが、新しいメソッドを追加するためには既存のコードを変更しなければなりません。関 数プログラミングスタイルは、新しいメソッドを容易に追加できますが、新しいデータを追 加するためには既存のコードを変更しなければなりません。 新しいメソッドを追加する 新しいデータを追加する オブジェクト指向 既存のコードを変更する 既存のコードを変更しない 関数プログラミング 既存のコードを変更しない 既存のコードを変更する
  52. 52. 4.5.5 「オブジェクト指向」対「関数的拡張性」 Scala はポリモーフィズムとパターンマッチの両方を使用できる柔軟性を持つので、どち らも適切に使用すべきです。しかしながら、コードの意味論について優れた保証を与え るシールドトレイトを一般的に好み、後述する型クラスを使用することでオブジェクト指向 スタイルの拡張性を手に入れることができます。
  53. 53. 4.6 再帰的データ とくに代数的データ型は再帰的データの定義によく使用されます。このデータはそれ自 身の項によって定義され、サイズを制約しないデータを作成します。 ただし、再帰が永遠に続くため、このようには定義できません。 final case class Broken(broken: Broken) 最後を表すベースケースを定義することで妥当な再帰的データを定義できます。 sealed trait IntList final case object End extends IntList final case class Pair(head: Int, tail: IntList) extends IntList Pair(1, Pair(2, Pair(3, End))) // res: Pair = Pair(1,Pair(2,Pair(3,End)))
  54. 54. 4.6 再帰的データ 再帰的代数的データ型を処理するために構造的再帰パターンを適用できます。 それでは IntList のすべての要素を加算してみましょう。まずは、メソッド定義とテストを 記述するところから始めます。 def sum(list: IntList): Int = ??? val example = Pair(1, Pair(2, Pair(3, End))) assert(sum(example) == 6) assert(sum(example.tail) == 5) assert(sum(End) == 0)
  55. 55. 4.6 再帰的データ メソッドの本体に構造的再帰パターンを適用してみましょう。 def sum(list: IntList): Int = list match { case End => ??? case Pair(hd, tl) => ??? }
  56. 56. 4.6 再帰的データ End の解答は 0 と決定できます。Pair は Int 型を返す必要があり、tl について再帰呼 び出しをする必要があります。 def sum(list: IntList): Int = list match { case End => 0 case Pair(hd, tl) => ??? sum(tl) }
  57. 57. 4.6 再帰的データ 再帰呼び出しは残りのリストの合計返すので、その結果に hd を加算します。 def sum(list: IntList): Int = list match { case End => 0 case Pair(hd, tl) => hd + sum(tl) }
  58. 58. 4.6.1 ベースケースと再帰ケースを理解する ベースケースと再帰ケースの本体を与える上で、一般的な指針があります。 ベースケースは、一般的にその計算における単位元を返します。例えば、加算の単位 元は0で、乗算の単位元は1です。 再帰ケースは、正しい結果を返す再帰と仮定し、正しい解答が得られるように実装を追 加しました。sum で見たように、再帰呼び出しが残りのリストについて正しい結果を与え ることを仮定し、リストの先頭をただ追加しました。
  59. 59. ノート:再帰的代数的データ型パターン 再帰的代数的データ型を定義するとき、ひとつは再帰的で、もうひとつは再帰的ではな い、という2つのケースが少なくとも必要である。ベースケースとして知られるものは再帰 的ではない。コードでの一般的なスケルトンは下記のとおりである。 sealed trait RecursiveExample final case class RecursiveCase(recursion: RecursiveExample) extends RecursiveExample final case object BaseCase extends RecursiveExample 74
  60. 60. ノート:再帰的構造的再帰パターン 再帰的代数的データ型で構造的再帰コードを記述するときは下記のような対応をする。 ● データにおいて再帰要素に遭遇するときはいつも、メソッドの再帰的呼び出しを行う ● データにおいてベースケースに遭遇するときはいつも、実行しようとしている計算の 単位元を返す 76
  61. 61. 4.6.2 末尾再帰 再帰によって大量のスタック領域を消費してしまうのではないかと心配するかもしれませ ん。Scala は末尾再帰と呼ばれる最適化を適用するため、多くの再帰関数はスタック領 域を消費し続けることはありません。 末尾呼び出しは、メソッドを呼び出した値をただちに返すメソッド呼び出しです。下記の tailCall は、method1 の結果をただちに返すので末尾呼び出しです。 def method1: Int = 1 def tailCall: Int = method1 下記の notTailCall は、数値を加算をしているため、 method1 の結果をただちに返さな いので末尾呼び出しではありません。 def notATailCall: Int = method1 + 2
  62. 62. 4.6.2 末尾再帰 末尾呼び出しはスタック領域を使用しないよう最適化できます。JVM の制約から Scala は自分自身を呼び出す末尾呼び出ししか最適化できません。よって末尾再帰は保守面 で重要で、@tailrec アノテーションを使用することでコンパイラーに末尾再帰になってい るかをチェックさせることができます。
  63. 63. 4.6.2 末尾再帰 import scala.annotation.tailrec @tailrec def sum(list: IntList): Int = list match { case End => 0 case Pair(hd, tl) => hd + sum(tl) } // <console>:18: error: could not optimize @tailrec annotated method sum: it ... // def sum(list: IntList): Int = list match { // ^ @tailrec def sum(list: IntList, total: Int = 0): Int = list match { case End => total case Pair(hd, tl) => sum(tl, total + hd) } // sum: (list: IntList, total: Int)Int
  64. 64. 4.6.2 末尾再帰 どんな非末尾再帰関数でも、前述の sum で実現したようにアキュムレーターを追加す ることで末尾再帰版に変換することができます。 Scala で末尾再帰関数を直接書いて処理することはあまりなく、Scala の充実したコレク ションライブラリを使用することで末尾再帰で実現したいことのほとんどを実現できます。
  65. 65. 4.7 大きな例 * 大きなプロジェクトの例です。 1. Calculator 2. JSON 3. Music * 訳注:課題の節なので説明は割愛します。
  66. 66. 4.8 まとめ 本章では重要な視点の変更を行い、言語機能から離れ、言語機能がサポートするプロ グラミングパターンに目を向けました。これは本稿の残りでも継続します。 2つの重要なパターン、代数的データ型と構造的再帰を探究しました。それらのパターン は、データのメンタルモデルから、まったく機械的な方法による Scala におけるデータの 表現と処理に我々を向かわせます。月並みなコードの構造や理解を容易にするだけで はなく、コンパイラーが開発や保守を容易にするために共通のエラーを捕捉してくれま す。これら2つのツールは、慣用的な関数的コードで一般に使用され、その重要性はど れほど強調してもしすぎることはありません。 本章における課題の中で共通のデータ構造を開発しましたが、固定された型のデータし か保持できなく、そのコードは多くの繰り返しが含まれていました。次章では、型とメソッ ドを超えた抽象化を見ていきながら、シーケンス処理におけるいくつかの重要なコンセプ トを紹介します。

×