無限大な夢のあと

テニスとアニメが大好きな厨二病SEのブログ

Domain-Drive Design Quicklyを読んで(自分用メモ) その1

DDD導入研修の課題として、下記の書籍「Domain-Drive Design Quickly」を読むことになりました。
Domain Driven Design(ドメイン駆動設計) Quickly 日本語版
本文章は上記の日本語訳の資料を引用させて頂きます。


ただ漠然と読んでいるだけだと、頭を通りすぎていきそうなので、自分なりに要点や響いた箇所、メモを残しながら読んでいこうと思います。
※とはいえ、約80ページしかないものではありますが。

DDDの世界への第一歩を踏み出します!

・0.イントロダクション

ソフトウエアの設計は芸術です。したがって芸術と同様、厳密な科学のように
定理や公式を利用して教わったり学んだりできません。私たちはソフトウエア
の作成過程全体に適用できるような、原則や技法を発見することはできます。

芸術。まだこのレベルまでは遥かに至っておりませぬ。。


・1.ドメイン駆動設計とは何か

ドメ インはとても多くの情報を含んでいるので、すべてをモデルに取り込めません。 また、ドメインの大部分は考慮する必要さえないでしょう。こういった取捨選 択自体が大きな課題となります。何をモデルに取り込み、何を捨てるのか。こ れが設計作業であり、ソフトウエアの作成です。銀行業務システムはきっと顧 客の住所録を保持するでしょうが、顧客の目の色は無視します。もっともこれ はわかりやすい例です。他の場合はこれほどわかりやすくないでしょう。

この取捨選択こそが難しいのである。

モデルはソフトウエアの核ですが、これを表現し他人に伝える方法が必要です。 私たちは一人で作業するわけではありません。上手に、正確に、完璧に、曖昧 さを排除して、知識と情報を共有しなければなりません。これにはいくつかの 方法があります。ひとつは図による表現、すなわちダイヤグラム、ユースケー ス、スケッチ、絵などです。もうひとつは記述による表現、つまりドメインに ついての見通しを文章で記述する方法です。そして独自言語で表現する方法も あります。ドメインについての特定の問題を伝達するために私たちは独自の言 語を作成することができますし、そうするベきなのです。これらの詳細は後述 しますが、もっとも大切なのは「モデルを他人に伝えなければならない」という ことです。

「モデルを他人に伝えなければならない」。どのような手段であれ、これが一番大切。

この本はドメイン駆動設計の原則を説明します。この設計原則を適用すれば、
どのような開発過程であれ、その能力を十分に発揮してドメインの複雑な問題
に対して継続的にモデリングし、実装できるようになります。ドメイン駆動設
計は設計と開発を兼ね備え、どのように設計と開発が協調すればよりよいソフ
トウエアが出来上がるのかを示しています。優れた設計は開発を加速させ、開
発からのフィードバックは設計の精度を高めるでしょう。

「優れた設計は開発を加速させ、開発からのフィードバックは設計の精度を高めるでしょう。」
より設計と開発側を結びつけることにより、ソフトウェアの複雑性に立ち向かうということかな。
最近、読んでる下記の書籍で紹介されている原則でも、大まかにはソフトウェアの複雑性に立ち向かうためのものが多い。



・2.ユビキタス言語

ドメイン駆動設計の核となる原則は、ドメインモデルに基づく言語を使うこと
です。ドメインモデルはソフトウエアとドメインが出会う場所にあるのですか
ら、共通言語の基盤として適切です。
共通言語の拠り所としてドメインモデルを使います。そしてこの言語をコミュ
ニケーションに、さらにはコードにも使うようにチームのメンバに要求しまし
ょう。知識を共有しドメインモデルを構築しているあいだにも、チーム内では
レビューをしたり文書や図を作ったりします。どんな形態のコミュニケーショ
ンであれ、常にこの言語で表現しましょう。このような性質からこの言語は
「ユビキタス言語」と呼ばれます。

今までの現場であったような業務に関する共通言語とは正確には違い、ドメインモデルに基づく言語を「ユビキタス言語」とすると。

ユビキタス言語を創造する

この困難な作業に取り組み始めるには、チームのすべてのメンバが共通言語を
作らなければならないと自覚し、重要な点に常に注目するように気をつけ、必
要なときはいつでも作成した共通言語を使うようにします。このような作業の
ときには、独自の専門用語を可能な限り使わないようにします。そしてユビキ
タス言語を使いましょう。ユビキタス言語は明確に、そして正確に意思伝達を
する手助けをしてくれるのですから。

ここは、実際の設計時には特に意識していきたいところ。
いきなり実践ではできないから、ここを何かしらの方法でトレーニングしていきたいな。

もちろん、コードを使ってコミュニケーションをすることも可能です。この方 法はXPプログラミングのコミュニティで広く支持されています。丁寧に書かれ たコードはコミュニケーションにとても適しています。しかし、例えばコード を読むことで、メソッドが表現する振る舞いが理解できても、そのメソッド名 がその振る舞いと同じくらい明瞭に理解できるでしょうか。テストのためのア サーションは、アサーション自体の内容を十分に伝えてくれますが、変数名や コードの構造全体については何か伝えてくれるでしょうか。すべての挙動を一 目瞭然に教えてくれるでしょうか。コードは正しく振る舞いますが、必ずしも 正しい表現をするわけではありません。コードを使ってモデルを表現するのは とても難しいことです。

コメント書きすぎても保守コストが高いので、現実問題どれだけわかりやすくしてもそうだろうなと実感。


3.モデル駆動設計

3-1.モデル駆動設計の基本要素

業務ドメインを中心に据えたソフトウエア開発手法の重要さを強調
しました。ドメインに深く根ざしたモデルをつくることがとても重要であり、
そのモデルは、ドメインの中心にある概念を正確に反映するべきだと説明しま
した。「ユビキタス言語」はモデリングの作業全体を通して、ソフトウエア開
発者とドメインの専門家とのコミュニケーションをとても楽にします。また、
モデルに取り込むべきドメインの中心概念の発見にも役立ちます。モデリング
の目的は、よいモデルを作成することです。そして次は、モデルをコードとし
て実装していく作業です。ソフトウエア開発においては、この作業もモデリン
グと同様に重要です。すばらしいドメインモデルを作成しても、コードの中へ
正確に移植できなければ、品質の悪いソフトウエアになってしまいます。

前章までの振り返り。
頭の整理のためにメモとして残しておく。

どのようなドメインでも様々なモデルで表せます。そしてどのようなモデルで
も様々な方法でコードに落とし込めます。どんな問題にもひとつ以上の解決方
法があります。ではどの方法を選べばいいのでしょうか。よく分析されている
正確なモデルが、必ずしもコードでそのまま表現できるモデルであるとはかぎ
りません。それどころか、ソフトウエア設計の原則を無視した実装になること
もあるでしょう。原則を無視するのは勧められません。重要なのは簡単に、そ
して正確にコードに落とし込めるモデルを選ぶことです。ではここで基本的な
質問です。私たちはどのような手法でモデルからコードへ変換するのでしょう
か。

今までやっていたのは、例えばレシートからモデルを考えるのやった時には、名詞に着目して用語を抜き出してそこから関係性を記述したくらいか。
手法の名前は忘れました。

この方法の主な問題点は、アナリストがモデルの欠点や複雑な点をすべて予見 できないことです。アナリストはモデルの一部の構成要素は詳しく分析しても、 残りは十分に分析していないかもしれません。このため、とても重要な細部が 設計や実装の段階になって初めて見つかってしまいます。仮にドメインを正確 にあらわしたモデルを使ってみれば、オブジェクトの永続化に深刻な問題があ ったり、性能が許容できないほど悪いことがわかるでしょう。

また、開発者は独自に決断をせざるを得ないことがあるでしょう。モデルを作
成した時には考慮していなかった問題を解決するために、設計を変更すること
もあります。モデルから抜け落ちてしまっている部分を設計するのですから、
さらにモデルとの関連性が希薄になります。

分析モデルの欠点の記述。
そもそも、完璧に近いものなんてそうそう難しい。

つまり、より良い方法はドメインモデリングと設計を密接にすることです。モ
デルを作成するときは、ソフトウエアとその設計を考慮するべきです。また開
発者もモデリングに参加すべきです。モデルに基づいた設計作業を後戻りする
ことなく進めるには、ソフトウエアを正確に表現するモデルを選択することが
重要です。コードが基礎になるモデルとしっかり結びついていれば、コードの
意味が明確になり、モデルもより適切になるでしょう。

一つの結論かな。
完璧な設計なんて難しいから設計への手戻りを許容すると。

コードを書く人はモデルをよく知り、モデルが完全であることに責任を感じる
べきです。そして、コードの変更はモデルの変更を伴うことを自覚しなければ
なりません。そうでないと元のモデルと無関係になるまで、コードとリファク
タリングしてしまうでしょう。また、実装に関心を示さないアナリストは、開
発中に初めて見つかった実装上の制限に興味を示さないでしょう。その結果、
モデルは実践に適したものではなくなります。
どんな技術者であれ、ドメインモデルの作成に関係するのであれば、いくらか
時間を割いてコードを触ってみなければなりません。たとえそのプロジェクト
を主導する役割を担っていても技術者であればそうすべきです。コードの変更
に責任がある者はだれでも、コードを通してモデルを表現することを学ばなけ
ればなりません。すべての開発者はドメインモデルについての議論に参加し、
ドメインの専門家と意見交換をする必要があります。プロジェクトのメンバは
様々な方法でドメインモデルの作成に関わりますが、彼らは実際にコードを触
る技術者とユビキタス言語を使って活発に意見を交換しなければなりません。

「モデルをよく知り、モデルが完全であることに責任を感じる。」
他の誰かが立ち上げたプロジェクトに入ることになることがほとんどなので、モデルの理解を深めつつ、修正時にはより気をつかわなければいけないと。

ドメインモデルを正しく設計に反映させるためには、ソフトウエアシステムを
部分ごとに設計します。そうすれば、ドメインモデルと設計の対応関係も明確
になります。また、ドメインモデルを見直して修正を加えることで、より自然
にソフトウエアを実装できるようにします。ドメインの詳細な内容をモデルに
表現しようとする場合も同様です。設計との対応関係が明確なモデルであるこ
と。より自然にソフトウエアを実装できるモデルであること。この二つを満た
すひとつのモデルが必要です。加えて、ユビキタス言語が十分に使われていな
ければなりません。

・「設計との対応関係が明確なモデルであること。」
・「より自然にソフトウエアを実装できるモデルであること。」
この二つを満たすことが必要。

ドメインモデルから設計に使われる用語を抜き出しましょう。また、モデルの
どの要素にどの責務を割り当てるのかも、大まかに考えておきます。コードは
ドメインモデルを表現するので、コードの変更はモデルの変更と同じです。そ
して、その変更の影響はプロジェクトのその他の作業にも、相応に波及してい
かなければなりません。

ここでいわゆるコード設計する形になるのかな。

実装とモデルを強く結びつけるためには、オブジェクト指向プログラミングの
ようなモデリングのパラダイムに基づいているプログラミング言語やソフトウ
エア開発ツールが必要です。
手続き型の言語はモデル駆動設計を十分にサポートしません。モデルの重要な 部分を実装するために必要な概念を提供しないからです。OOPC言語のよう な手続き型言語と同じ使い方ができると言う人もいます。そして実際にOOPの 機能を使えば手続き型言語と同じような使い方もできます。例えば、オブジェ クトをデータ構造とみなして、振る舞いを割り当てないようにします。そして、 振る舞いはファンクションとしてオブジェクトとは別に実装します。しかし、 こうするとデータの意味は開発者にしかわかりません。コードそのものが意味 をはっきりと表現しないからです。手続き型の言語で書かれたプログラムは、 ファンクションの集合として理解されます。プログラムを走らせれば、ひとつ のファンクションが別のファンクションを呼び出しながら処理を進めて結果を 出力します。このようなプログラムでは、概念的に関係のあるプログラムの要 素をカプセル化できません。また、ドメインとコードを対応づけるのも難しい です。
手続き型言語を使って簡単にモデリングし実装できる特定の領域もあります。 例えば数学です。ほとんどの数学理論は計算で表現できるので、ファンクショ ンの呼び出しとデータ構造を使って単純に処理を実行できます。しかし、複雑 なドメインは計算のような抽象的な概念を集めたものではなく、アルゴリズム の集合に単純化できません。したがって、手続き型言語は様々な種類のドメイ ンを表現するには役不足です。モデル駆動設計での手続き型言語の使用は推奨 しません。

「モデル駆動設計での手続き型言語の使用は推奨 しません」とのこと。

モデル駆動設計の基本要素がここから紹介されていくので、一旦別記事で紹介していきます。

エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)

エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)

実践ドメイン駆動設計 (Object Oriented SELECTION)

実践ドメイン駆動設計 (Object Oriented SELECTION)

ドワンゴの新卒エンジニア向けの研修資料でScalaに入門 その8(Scalaのコレクションライブラリ(immutableとmutable)前半

Scalaを業務で使うことになり、以下のドワンゴオリジナルの新卒エンジニア向けの研修資料でScalaを本格的に勉強してみることにした。
https://dwango.github.io/scala_text/index.htmldwango.github.io

可能な限りGolangとの比較も入れていきます。

学んだことをとにかく走り書きしていきます。
Scalaのコレクションライブラリ(immutableとmutable)】
・概要

immutableなコレクションを使うのにはいくつものメリットがあります

関数型プログラミングで多用する再帰との相性が良い
高階関数を用いて簡潔なプログラムを書くことができる
一度作ったコレクションが知らない箇所で変更されていない事を保証できる
並行に動作するプログラムの中で、安全に受け渡しすることができる
mutableなコレクションを効果的に使えばプログラムの実行速度を上げることができますが、mutableなコレクションをどのような場面で使えばいいかは難しい問題です。

この節では、Scalaのコレクションライブラリに含まれる以下のものについての概要を説明します。

Array(mutable)
List(immutable)
Map(immutable)・Map(mutable)
Set(immutable)・ Set(mutable)

メリットがまとめられているのでわかりやすい。
再帰を使いこなせるようになることを、簡潔なプログラムを書けるように勉強していきたい。

・Array

まずは大抵のプログラミング言語にある配列です。

scala> val arr = Array(1, 2, 3, 4, 5)
arr: Array[Int] = Array(1, 2, 3, 4, 5)

これで1から5までの要素を持った配列がarrに代入されました。Scalaの配列は、他の言語のそれと同じように要素の中身を入れ替えることができます。配列の添字は0から始まります。なお、配列の型を指定しなくて良いのは、Array(1, 2, 3, 4, 5)の部分で、要素型がIntであるに違いないとコンパイラ型推論してくれるからです。型を省略せずに書くと

scala> val arr = Array[Int](1, 2, 3, 4, 5)
arr: Array[Int] = Array(1, 2, 3, 4, 5)

となります。ここで、[Int]の部分は型パラメータと呼びます。Arrayだけだとどの型かわからないので、[Int]を付けることでどの型のArrayかを指定しているわけです。この型パラメータは型推論を補うために、色々な箇所で出てくるので覚えておいてください。しかし、この場面では、Arrayの要素型はIntだとわかっているので、冗長です。次に要素へのアクセスと代入です。

scala> arr(0) = 7

scala> arr
res1: Array[Int] = Array(7, 2, 3, 4, 5)

scala> arr(0)
res2: Int = 7

他の言語だとarr[0]のようにしてアクセスすることが多いので最初は戸惑うかもしれませんが、慣れてください。配列の0番目の要素がちゃんと7に入れ替わっていますね。

配列の長さはarr.lengthで取得することができます。

scala> arr.length
res3: Int = 5

Array[Int]はJavaではint[]と同じ意味です。Scalaでは、配列などのコレクションの要素型を表記するとき Collection[ElementType]のように一律に表記し、配列も同じように記述するのです。Javaでは配列型だけ特別扱いするのに比べると統一的だと言えるでしょう。

うん、ここは文法上の話。
使うことになったら、再度確認するくらいで良いかな。

ただし、あくまでも表記上はある程度統一的に扱えますが、実装上はJVMの配列であり、 要素が同じでもequalsの結果がtrueにならない, 生成する際にClassTagというものが必要 などのいくつかの罠があるので、Arrayはパフォーマンス上必要になる場合以外はあまり積極的に使うものではありません。

これは頭に留めておきたい。
ClassTagと言うものについては以下を参照。
Scala ClassTagメモ(Hishidama's Scala ClassTag Memo)

Arrayの練習問題
配列のi番目の要素とj番目の要素を入れ替えるswapArrayメソッドを定義してみましょう。swapArrayメソッドの宣言は

def swapArray[T](arr: Array[T])(i: Int, j: Int): Unit = ???

となります。iとjが配列の範囲外である場合は特に考慮しなくて良いです。

Javaの時にやったような配列の入れ替えの実装。




・Range

Rangeは範囲を表すオブジェクトです。Rangeは直接名前を指定して生成するより、toメソッドとuntilメソッドを用いて呼びだすことが多いです。また、toListメソッドを用いて、その範囲の数値の列を後述するListに変換することができます。では、早速REPLでRangeを使ってみましょう。

scala> 1 to 5
res8: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5)

scala> (1 to 5).toList
res9: List[Int] = List(1, 2, 3, 4, 5)

scala> 1 until 5
res10: scala.collection.immutable.Range = Range(1, 2, 3, 4)

scala> (1 until 5).toList
res11: List[Int] = List(1, 2, 3, 4)

toは右の被演算子を含む範囲を、untilは右の被演算子を含まない範囲を表していることがわかります。また、RangeはtoListで後述するListに変換することができることもわかります。

rangeはGolangにもあるし、JavaだとGuavaのライブラリにもあった気がする。
untilでは含まないということだけ覚えておこう。

・List

さて、導入として大抵の言語にあるArrayを出しましたが、ScalaではArrayを使うことはそれほど多くありません。代わりにListや Vectorといったデータ構造をよく使います(Vectorについては後述します)。Listの特徴は、一度作成したら中身を変更できない(immutable)ということです。中身を変更できないデータ構造(永続データ構造とも呼びます)はScalaがサポートしている関数型プログラミングにとって重要な要素です。それではListを使ってみましょう。

scala> val lst = List(1, 2, 3, 4, 5)
lst: List[Int] = List(1, 2, 3, 4, 5)
scala> lst(0) = 7
<console>:14: error: value update is not a member of List[Int]
       lst(0) = 7
       ^

見ればわかるように、Listは一度作成したら値を更新することができません。しかし、Listは値を更新することができませんが、あるListを元に新しいListを作ることができます。これが値を更新することの代わりになります。以降、Listに対して組み込みで用意されている各種操作をみていくことで、Listの値を更新することなく色々な操作ができることがわかるでしょう。

JavaだとGuavaで使っているImmutableListと同じなので、使う分には問題なし。

Nil:空のList

まず最初に紹介するのはNilです。Scalaで空のListを表すにはNilというものを使います。Rubyなどではnilは言語上かなり特別な意味を持ちますが、Scalaではデフォルトでスコープに入っているということ以外は特別な意味はなく単にobjectです。Nilは単体では意味がありませんが、次に説明する::と合わせて用いることが多いです。

デフォルトでスコープに入っているというのがあまり理解できず。

・:: - Listの先頭に要素をくっつける

(コンスと読みます)は既にあるListの先頭に要素をくっつけるメソッドです。これについては、REPLで結果をみた方が早いでしょう。
scala> val a1 = 1 :: Nil
a1: List[Int] = List(1)

scala> val a2 = 2 :: a1
a2: List[Int] = List(2, 1)

scala> val a3 = 3 :: a2
a3: List[Int] = List(3, 2, 1)

scala> val a4 = 4 :: a3
a4: List[Int] = List(4, 3, 2, 1)

scala> val a5 = 5 :: a3
a5: List[Int] = List(5, 3, 2, 1)

付け足したい要素を::を挟んでListの前に書くことでListの先頭に要素がくっついていることがわかります。ここで、::はやや特別な呼び出し方をするメソッドであることを説明しなければなりません。まず、Scalaでは1引数のメソッドは中置記法で書くことができます。それで、1 :: Nil のように書くことができるわけです。次に、メソッド名の最後が:で終わる場合、被演算子の前と後ろをひっくり返して右結合で呼び出します。たとえば、

scala> 1 :: 2 :: 3 :: 4 :: Nil
res13: List[Int] = List(1, 2, 3, 4)

は、実際には、

scala> Nil.::(4).::(3).::(2).::(1)
res14: List[Int] = List(1, 2, 3, 4)

のように解釈されます。Listの要素が演算子の前に来て、一見数値のメソッドのように見えるのにListのメソッドとして呼び出せるのはそのためです。

Listの後ろにくっつけるのではなく、先頭にくっつけるメソッドの説明が先なのはNilの説明の都合上かな?

・++:List同士の連結

    1. はList同士を連結するメソッドです。これもREPLで見た方が早いでしょう。
scala> List(1, 2) ++ List(3, 4)
res15: List[Int] = List(1, 2, 3, 4)

scala> List(1) ++ List(3, 4, 5)
res16: List[Int] = List(1, 3, 4, 5)

scala> List(3, 4, 5) ++ List(1)
res17: List[Int] = List(3, 4, 5, 1)
    1. は1引数のメソッドなので、中置記法で書いています。また、末尾が:で終わっていないので、たとえば、
scala> List(1, 2) ++ List(3, 4)
res18: List[Int] = List(1, 2, 3, 4)

scala> List(1, 2).++(List(3, 4))
res19: List[Int] = List(1, 2, 3, 4)

と同じ意味です。大きなList同士を連結する場合、計算量が大きくなるのでその点には注意した方が良いです。

計算量は注意しよう。

・mkString:文字列のフォーマッティング

このメソッドはScalaで非常に頻繁に使用され皆さんも、Scalaを使っていく上で使う機会が多いであろうメソッドです。このメソッドは引数によって多重定義されており、3バージョンあるのでそれぞれを紹介します。

mkString

引数なしバージョンです。このメソッドは、単にListの各要素を左から順に繋げた文字列を返します。

scala> List(1, 2, 3, 4, 5).mkString
res20: String = 12345

注意しなければならないのは、引数なしメソッドのmkStringは()を付けて呼びだすことができない という点です。たとえば、以下のコードは、若干分かりにくいエラーメッセージがでてコンパイルに失敗します。

scala> List(1, 2, 3, 4, 5).mkString()
<console>:13: error: overloaded method value mkString with alternatives:
  => String <and>
  (sep: String)String <and>
  (start: String,sep: String,end: String)String
 cannot be applied to ()
       List(1, 2, 3, 4, 5).mkString()

Scalaの0引数メソッドは()なしと ()を使った定義の二通りあって、前者の形式で定義されたメソッドは()を付けずに呼び出さなければいけません。逆に、()を使って定義されたメソッドは、()を付けても付けなくても良いことになっています。このScalaの仕様は混乱しやすいので注意してください。

これは確かにはまりそう。Scalaの仕様としても覚えておこう。

mkString(sep: String)

引数にセパレータ文字列sepを取り、Listの各要素をsepで区切って左から順に繋げた文字列を返します。

scala> List(1, 2, 3, 4, 5).mkString(",")
res22: String = 1,2,3,4,5

mkString(start: String, sep: String, end: String)

mkString(sep)とほとんど同じですが、startとendに囲まれた文字列を返すところが異なります。

scala> List(1, 2, 3, 4, 5).mkString("[", ",", "]")
res23: String = [1,2,3,4,5]

ここは問題なし。

・mkstringの練習問題

mkStringを使って、最初の数startと最後の数endを受け取って、

start,...,end

となるような文字列を返すメソッドjoinByCommaを定義してみましょう(ヒント:Range にもmkStringメソッドはあります)。

def joinByComma(start: Int, end: Int): String = {
  (start to end).mkString

}

ここも問題なし。

・foldLeft:左からの畳み込み

foldLeftメソッドはListにとって非常に基本的なメソッドです。他の様々なメソッドをfoldLeftを使って実装することができます。foldLeftの宣言をScalaAPIドキュメントから引用すると、

def foldLeft[B](z: B)(f: (B, A) ⇒ B): B

となります。zがfoldLeftの結果の初期値で、リストを左からたどりながらfを適用していきます。foldLeftについてはイメージが湧きにくいと思いますので、List(1, 2, 3).foldLeft(0)((x, y) => x + y)の結果を図示します。

       +
      / \
     +   3
    / \
   +   2
  / \
 0   1

この図で、

  +
  / \
 0   1

は+に0と1を与えて適用するということを意味します。リストの要素を左から順にfを使って「畳み込む」(fold は英語で畳み込むという意味を持ちます)状態がイメージできるでしょうか。foldLeftは汎用性の高いメソッドで、たとえば、Listの要素の合計を求めたい場合は

scala> List(1, 2, 3).foldLeft(0)((x, y) => x + y)
res25: Int = 6

Listの要素を全て掛けあわせた結果を求めたい場合は

scala> List(1, 2, 3).foldLeft(1)((x, y) => x * y)
res26: Int = 6

とすることで求める結果を得ることができます1。その他にも様々な処理をfoldLeftを用いて実装することができます。

うん、ここはUndersocre.jsで勉強した時にふわっとやったので覚えてる。
使いこなせるよう問題をいくつか解きたい。

foldLeftの練習問題

foldLeftを用いて、Listの要素を反転させる次のシグニチャを持ったメソッドreverseを実装してみましょう:

def reverse[T](list: List[T]): List[T] = list.foldLeft(Nil: List[T])((a, b) => b :: a)

OK。



・foldRight:右からの畳み込み

foldLeftがListの左からの畳み込みだったのに対して、foldRightは右からの畳込みです。foldRightの宣言を ScalaAPIドキュメントから参照すると、

def foldRight[B](z: B)(op: (A, B) ⇒ B): B

となります。foldRightに与える関数であるopの引数の順序がfoldLeftの場合と逆になっている事に注意してください。 foldRightをList(1, 2, 3).foldRight(0)((y, x) => y + x)とした場合の様子を図示すると次のようになります

  +
  / \
 1   +   
    / \
   2   +   
      / \
     3   0

ちょうどfoldLeftと対称になっています。foldRightも非常に汎用性の高いメソッドで、多くの処理をfoldRightを用いて実装することができます。

foldLeftとの使い分けがそこまでわからず。


foldRightの練習問題 その1

Listの全ての要素を足し合わせるメソッドsumをfoldRightを用いて実装してみましょう。sumの宣言は次のようになります。なお、Listが空のときは0を返してみましょう。

scala>  def sum(list: List[Int]): Int = list.foldRight(0)((a,b) => a+b)

すんなりいけたので、問題なし。


foldRightの練習問題 その2

Listの全ての要素を掛け合わせるメソッドmulをfoldRightを用いて実装してみましょう。mulの宣言は次のようになります。なお、Listが空のときは1を返してみましょう。

scala> def mul(list: List[Int]): Int = list.foldRight(1)((a,b) =>a * b)
mul: (list: List[Int])Int

これもすんなりいけたので、問題なし。

foldRightの練習問題 その3

mkStringを実装してみましょう。mkStringそのものを使ってはいけませんが、foldLeftやfoldRightなどのListに定義されている他のメソッドは自由に使って構いません。ListのAPIリファレンス を読めば必要なメソッドが載っています。実装するmkStringの宣言は

scala>def mkString[T](list: List[T])(sep: String): String = list match {
  case Nil => ""
  case x::xs => xs.foldLeft(x.toString){(x, y) => x + sep + y}
}

ここは解けず。
回答例を参考にする。
x:xsがListを表すことができて、case matchで引っかかるそう。
9.1 リストの使用 (Using Lists) - プログラミング言語Scala 日本語情報サイト


引き続き頑張る。

Scalaスケーラブルプログラミング第3版

Scalaスケーラブルプログラミング第3版

Programming in Scala: Updated for Scala 2.12

Programming in Scala: Updated for Scala 2.12

Scalaパズル 36の罠から学ぶベストプラクティス

Scalaパズル 36の罠から学ぶベストプラクティス

SCALAプログラミング入門

SCALAプログラミング入門

ドワンゴの新卒エンジニア向けの研修資料でScalaに入門 その7(関数)

関数型のパラダイムを学んで業務に活かそうということで、以下のドワンゴオリジナルの新卒エンジニア向けの研修資料でScalaを本格的に勉強してみることにした。
dwango.github.io
※Java8でごにょごにょしないのは、Scalaを趣味でやりたいという意図があるだけです。

どうせなら、Scalaの言語仕様からガッツリ学んで自分のものにしたい。
また、可能な限りGolangとの比較も入れていきます。

学んだことをとにかく走り書きしていきます。
【関数】
Scalaの関数

Scalaの関数は、他の言語の関数と扱いが異なります。Scalaの関数は単に Function0 〜 Function22 までのトレイトの無名サブクラスのインスタンスなのです。

たとえば、2つの整数を取って加算した値を返すadd関数は次のようにして定義することができます

scala> val add = new Function2[Int, Int, Int]{
     |   def apply(x: Int, y: Int): Int = x + y
     | }
add: (Int, Int) => Int = <function2>

scala> add.apply(100, 200)
res0: Int = 300

scala> add(100, 200)
res1: Int = 300

Function0からFunction22までの全ての関数は引数の数に応じたapplyメソッドを定義する必要があります。 applyメソッドはScalaコンパイラから特別扱いされ、x.apply(y)は常にx(y)のように書くことができます。後者の方が関数の呼び方としては自然ですね。

また、関数を定義するといっても、単にFunction0からFunction22までのトレイトの無名サブクラスのインスタンスを作っているだけです。

関数の作り方は理解。
確かもっと簡単に作れていた印象。
省略しないで書くと、このような処理をしているのか。
トレイトの無名サブクラスのインスタンス
無名サブクラスと言われてもピンとこない。。

・無名関数

前項でScalaで関数を定義しましたが、これを使ってプログラミングをするとコードが冗長になり過ぎます。そのため、 ScalaではFunction0〜Function22までのトレイトのインスタンスを生成するためのシンタックスシュガーが用意されています。たとえば、先ほどのadd関数は

scala> val add = (x: Int, y: Int) => x + y
add: (Int, Int) => Int = <function2>

と書くことができます。ここで、addには単に関数オブジェクトが入っているだけであって、関数本体には何の名前も付いていないことに注意してください。この、addの右辺のような定義をScalaでは無名関数と呼びます。無名関数は単なるFunctionNオブジェクトですから、自由に変数や引数に代入したり返り値として返すことができます。このような、関数を自由に変数や引数に代入したり返り値として返すことができる性質を指して、Scalaでは関数が第一級の値(First Class Object)であるといいます。

無名関数の一般的な構文は次のようになります。

(n1: N1, n2: N2, n3: N3, ...nn: NN) => B

n1からnnまでが仮引数の定義でN1からNNまでが仮引数の型です。Bは無名関数の本体です。無名関数の返り値の型は通常は Bの型から推論されます。先ほど述べたように、Scalaの関数はFunction0〜Function22までのトレイトの無名サブクラスのインスタンスですから、引数の最大個数は22個になります。

JavaScriptの無名関数と同じイメージ。
関数が「First Class Object」であるというのはこういうことか。
ちなみに、Golangも関数は「First Class Object」です。
また、Golangジェネリクスがないので、リフレクションで頑張らないと関数型の汎用的なmap関数やreduce関数のようなことはできないのです。

・関数の型

このようにして定義した関数の型は、本来はFunctionN[...]のようにして記述しなければいけませんが、関数の型については特別にシンタックスシュガーが設けられています。一般に、

(n1: N1, n2: N2, n3: N3, ...nn: NN) => B

となるような関数の型はFunctionN[N1, N2, N3, ...NN, Bの型]と書く代わりに

(N1, N2, N3, ...NN) => Bの型

として記述することができます。直接FunctionNを型として使うことは稀なので、こちらのシンタックスシュガーを覚えておくと良いでしょう。

具体例のサンプルが欲しかった。
シンタックスシュガーの記述に関しては後ほど、コップ本で調べる。

・関数のカリー化

関数型言語ではカリー化というテクニックがよく使われます。カリー化とは、たとえば (Int, Int) => Int 型の関数のように複数の引数を取る関数があったとき、これを Int => Int => Int 型の関数のように、1つの引数を取り、残りの引数を取る関数を返す関数のチェインで表現するというものです。試しに上記のaddをカリー化してみましょう。

scala> val add = (x: Int, y: Int) => x + y
add: (Int, Int) => Int = <function2>

scala> val addCurried = (x: Int) => ((y: Int) => x + y)
addCurried: Int => (Int => Int) = <function1>

scala> add(100, 200)
res2: Int = 300

scala> addCurried(100)(200)
res3: Int = 300

無名関数を定義する構文をネストさせて使っているだけで、何も特別なことはしていないことがわかります。

また、Scalaではメソッドの引数リストを複数に分けることで簡単にカリー化された関数を得ることができます。このことをREPLを用いて確認してみましょう。

scala> def add(x: Int, y: Int): Int = x + y
add: (x: Int, y: Int)Int

scala> add _
res0: (Int, Int) => Int = <function2>

scala> def addCurried(x: Int)(y: Int): Int = x + y
addCurried: (x: Int)(y: Int)Int

scala> addCurried _
res1: Int => (Int => Int) = <function1>

引数リストを複数に分けたaddCurriedから得られた関数は1引数関数のチェインになっていて、確かにカリー化されています。

Scalaのライブラリの中にはカリー化された形式の関数を要求するものがあったりするので、とりあえず技法として覚えておくのが良いでしょう。

メソッドの引数リストを複数に分けることで簡単にカリー化された関数を得ることができるのか。
Underscore.jsではそこまで便利ではなかったかなー
以下の書籍で少しだけ関数型の勉強はしたことがあったが、途中で挫折して積読になってしまっていた。。

JavaScriptで学ぶ関数型プログラミング

JavaScriptで学ぶ関数型プログラミング

また、Golangではシンタックスシュガーは用意されていなかった。
以下の記事でカリー化のサンプルがあった。
Go言語で関数のカリー化(currying)入門 - Qiita

・メソッドと関数の違い

メソッドについては既に説明しましたが、メソッドと関数の違いについてはScalaを勉強する際に注意する必要があります。本来はdefで始まる構文で定義されたものだけがメソッドなのですが、説明の便宜上、所属するオブジェクトの無いメソッド(今回は説明していません)やREPLで定義したメソッドを関数と呼んだりすることがあります。書籍やWebでもこの2つを意図的に、あるいは無意識に混同している例が多々あるので(Scalaバイブル『Scalaスケーラブルプログラミング』でも意図的なメソッドと関数の混同の例がいくつかあります)注意してください。

再度強調すると、メソッドはdefで始まる構文で定義されたものであり、それを関数と呼ぶのはあくまで説明の便宜上であるということです。ここまでメソッドと関数の違いについて強調してきましたが、それは、メソッドは第一級の値ではないのに対して関数は第一級の値であるという大きな違いがあるからです。メソッドを取る引数やメソッドを返す関数、メソッドが入った変数といったものはScalaには存在しません。

ここはしっかりと押さえたいところ。

高階関数

関数を引数に取ったり関数を返すメソッドや関数のことを高階関数と呼びます。先ほどメソッドと関数の違いについて説明したばかりなのに、メソッドのことも関数というのはいささか奇妙ですが、慣習的にそう呼ぶものだと思ってください。

早速高階関数の例についてみてみましょう。

scala> def double(n: Int, f: Int => Int): Int = {
     |   f(f(n))
     | }
double: (n: Int, f: Int => Int)Int

これは与えられた関数fを2回nに適用する関数doubleです。ちなみに、高階関数に渡される関数は適切な名前が付けられないことも多く、その場合はfやgなどの1文字の名前をよく使います。他の関数型プログラミング言語でも同様の慣習があります。呼び出しは次のようになります。

scala> double(1, m => m * 2)
res4: Int = 4

scala> double(2, m => m * 3)
res5: Int = 18

scala> double(3, m => m * 4)
res6: Int = 48

最初の呼び出しは1に対して、与えられた引数を2倍する関数を渡していますから、1 * 2 * 2 = 4になります。2番めの呼び出しは2に対して、与えられた引数を3倍する関数を渡していますから、2 * 3 * 3 = 18になります。最後の呼び出しは、3に対して与えられた引数を4倍する関数を渡していますから、3 * 4 * 4 = 48になります。

上記のようなサンプルだと、累乗計算を始め計算系の処理が楽にできそうなイメージ。
高階関数に渡される関数の名前が適切ではない1文字の名前が渡されるのは覚えておこう。

もう少し意味のある例を出してみましょう。プログラムを書くとき、

初期化
何らかの処理
後始末処理
というパターンは頻出します。これをメソッドにした高階関数aroundを定義します。

scala> def around(init: () => Unit, body: () => Any, fin: () => Unit): Any = {
     |   init()
     |   try {
     |     body()
     |   } finally {
     |     fin()
     |   }
     | }
around: (init: () => Unit, body: () => Any, fin: () => Unit)Any

try-finally 構文は、後の例外処理の節でも出てきますが、大体Javaのそれと同じだと思ってください。このaround関数は次のようにして使うことができます。

scala> around(
     |   () => println("ファイルを開く"),
     |   () => println("ファイルに対する処理"),
     |   () => println("ファイルを閉じる")
     | )
ファイルを開くファイルに対する処理ファイルを閉じる
res7: Any = ()

aroundに渡した関数が順番に呼ばれていることがわかります。ここで、bodyの部分で例外を発生させてみます。throwはJavaのそれと同じで例外を投げるための構文です。

scala> around(
     |   () => println("ファイルを開く"),
     |   () => throw new Exception("例外発生!"),
     |   () => println("ファイルを閉じる")
     | )
ファイルを開くファイルを閉じる
java.lang.Exception: 例外発生!
  at $anonfun$3.apply(<console>:16)
  at $anonfun$3.apply(<console>:16)
  at .around(<console>:15)
  ... 906 elided

のそれぞれを部品化して、「何らかの処理」の部分で異常が発生しても必ず後始末処理を実行できています。このaroundメソッドは1〜3の手順を踏む様々な処理に流用することができます。一方、1〜3のそれぞれは呼び出し側で自由に与えることができます。このように処理を値として部品化することは高階関数を定義する大きなメリットの1つです。

ちなみに、Java 7では後始末処理を自動化するtry-with-resources文が言語として取り入れられましたが、高階関数のある言語では、言語に頼らず自分でそのような働きをするメソッドを定義することができます。

後のコレクションの節を読むことで、高階関数のメリットをより具体的に理解できるようになるでしょう。

高階関数の使いどころをよくわかっていなかったけど、このような形で使うこともできるんだ。


Programming in Scala: Updated for Scala 2.12

Programming in Scala: Updated for Scala 2.12

Scalaパズル 36の罠から学ぶベストプラクティス

Scalaパズル 36の罠から学ぶベストプラクティス

SCALAプログラミング入門

SCALAプログラミング入門

ドワンゴの新卒エンジニア向けの研修資料でScalaに入門 その6(型パラメータと変位指定)

関数型のパラダイムを学んで業務に活かそうということで、以下のドワンゴオリジナルの新卒エンジニア向けの研修資料でScalaを本格的に勉強してみることにした。
dwango.github.io
※Java8でごにょごにょしないのは、Scalaを趣味でやりたいという意図があるだけです。

どうせなら、Scalaの言語仕様からガッツリ学んで自分のものにしたい。
また、可能な限りGolangとの比較も入れていきます。

学んだことをとにかく走り書きしていきます。
【型パラメータ】
気になった部分を以下に記載

クラスの節では触れませんでしたが、クラスは0個以上の型をパラメータとして取ることができます。これは、クラスを作る時点では何の型か特定できない場合(たとえば、コレクションクラスの要素の型)を表したい時に役に立ちます。型パラメータを入れたクラス定義の文法は次のようになります

class クラス名[型パラメータ1, 型パラメータ2, ..., 型パラメータN](コンストラクタ引数1 :コンストラクタ引数1の型, コンストラクタ引数2 :コンストラクタ引数2の型, ...)
{
  0個以上のフィールドの定義またはメソッド定義
}

型パラメータ1から型パラメータNまでは好きな名前を付け、クラス定義の中で使うことができます。とりあえず、簡単な例として、1個の要素を保持して、要素を入れる(putする)か取りだす(getする)操作ができるクラスCellを定義してみます。Cellの定義は次のようになります。

class Cell[T](var value: T) {
  def put(newValue: T): Unit = {
    value = newValue
  }

  def get(): T = value
}

これをREPLで使ってみましょう。

scala> class Cell[T](var value: T) {
     |   def put(newValue: T): Unit = {
     |     value = newValue
     |   }
     |   
     |   def get(): T = value
     | }
defined class Cell

scala> val cell = new Cell[Int](1)
cell: Cell[Int] = Cell@192aaffb

scala> cell.put(2)

scala> cell.get()
res1: Int = 2

scala> cell.put("something")
<console>:10: error: type mismatch;
 found   : String("something")
 required: Int
              cell.put("something")
                       ^
scala> val cell = new Cell[Int](1)
cell: Cell[Int] = Cell@6a01a75b

で、型パラメータとしてInt型を与えて、その初期値として1を与えています。型パラメータにIntを与えてCellをインスタンス化したため、REPLではStringをputしようとして、コンパイラにエラーとしてはじかれています。Cellは様々な型を与えてインスタンス化したいクラスであるため、クラス定義時には特定の型を与えることができません。そういった場合に、型パラメータは役に立ちます。

うん、ここまではJavaの型クラスと変わりはないかな?というイメージ。

次に、もう少し実用的な例をみてみましょう。メソッドから複数の値を返したい、という要求はプログラミングを行う上でよく発生します。そのような場合、型パラメータが無い言語では、

・片方を返り値として、もう片方を引数を経由して返す
・複数の返り値専用のクラスを必要になる度に作る
という選択肢しかありませんでした。しかし、前者は引数を返り値に使うという点で邪道ですし、後者の方法は多数の引数を返したい、あるいは解く問題上で意味のある名前の付けられるクラスであれば良いですが、ただ2つの値を返したいといった場合には小回りが効かず不便です。こういう場合、型パラメータを2つ取るPairクラスを作ってしまいます。Pairクラスの定義は次のようになります。toStringメソッドの定義は後で表示のために使うだけなので気にしないでください。

class Pair[T1, T2](val t1: T1, val t2: T2) {
  override def toString(): String = "(" + t1 + "," + t2 + ")"
}

このクラスPairの利用法としては、たとえば割り算の商と余りの両方を返すメソッドdivideが挙げられます。divideの定義は次のようになります。

def divide(m: Int, n: Int): Pair[Int, Int] = new Pair[Int, Int](m / n, m % n)

これらをREPLにまとめて流し込むと次のようになります。

scala> class Pair[T1, T2](val t1: T1, val t2: T2) {
     |   override def toString(): String = "(" + t1 + "," + t2 + ")"
     | }
defined class Pair

scala> def divide(m: Int, n: Int): Pair[Int, Int] = new Pair[Int, Int](m / n, m % n)
divide: (m: Int, n: Int)Pair[Int,Int]

scala> divide(7, 3)
res0: Pair[Int,Int] = (2,1)

7割る3の商と余りがres0に入っていることがわかります。なお、ここではnew Pair[Int, Int](m / n, m % n)としましたが、引数の型から型パラメータの型を推測できる場合、省略できます。この場合、Pairのコンストラクタに与える引数はIntとIntなので、new Pair(m / n, m % n)としても同じ意味になります。このPairは2つの異なる型(同じ型でも良い)を返り値として返したい全ての場合に使うことができます。このように、どの型でも同じ処理を行う場合を抽象化できるのが型パラメータの利点です。

ちなみに、このPairのようなクラスはScalaではよく使われるため、Tuple1からTuple22(Tupleの後の数字は要素数)があらかじめ用意されています。また、インスタンス化する際も、

scala> val m = 7
m: Int = 7

scala> val n = 3
n: Int = 3

scala> new Tuple2(m / n, m % n)
res1: (Int, Int) = (2,1)

などとしなくても、

scala> val m = 7
m: Int = 7

scala> val n = 3
n: Int = 3

scala> (m / n, m % n)
res2: (Int, Int) = (2,1)

とすれば良いようになっています。

うん、タプルでこのようなことができるのは把握した。
標準クラスで準備されているのは嬉しい。
Golangでは2値を返すメソッドが作れたりする。
成功時の結果と失敗時のエラーを常に返すメソッドを作るイメージ。


【変位指定(variance)】
・共変(covariant)

Scalaでは、何も指定しなかった型パラメータは通常は非変(invariant)になります。非変というのは、型パラメータを持ったクラスG、型パラメータT1とT2があったとき、T1 = T2のときにのみ

val : G[T1] = G[T2]

というような代入が許されるという性質を表します。これは、違う型パラメータを与えたクラスは違う型になることを考えれば自然な性質です。ここであえて非変について言及したのは、Javaの組み込み配列クラスは標準で非変ではなく共変であるという設計ミスを犯しているからです。

ここでまだ共変について言及していなかったので、簡単に定義を示しましょう。共変というのは、型パラメータを持ったクラスG、型パラメータT1とT2があったとき、T1 が T2 を継承しているときにのみ、

val : G[T2] = G[T1]
class G[+T]

のように型パラメータの前に+を付けるとその型パラメータは(あるいはそのクラスは)共変になります。

このままだと定義が抽象的でわかりづらいかもしれないので、具体的な例として配列型を挙げて説明します。配列型はJavaでは共変なのに対してScalaでは非変であるという点において、面白い例です。まずはJavaの例です。G = 配列、 T1 = String, T2 = Objectとして読んでください。

Object[] objects = new String[1];
objects[0] = 100;

このコード断片はJavaのコードとしてはコンパイルを通ります。ぱっと見でも、Objectの配列を表す変数にStringの配列を渡すことができるのは理にかなっているように思えます。しかし、このコードを実行すると例外 java.lang.ArrayStoreException が発生します。これは、objectsに入っているのが実際にはStringの配列(Stringのみを要素として持つ)なのに、2行目でint型(ボクシング変換されてInteger型)の値である100を渡そうとしていることによります。

共変や非変という概念を初めて聞いたので、ここは勉強になりました。
コンパイルではなく、実行時例外で発生するのは辛い。。
最近見た例だと、JDK のCollections.unmodifiableList()の実行時例外とかか。
以下の記事の中で言及されています。
qiita.com

一方、Scalaでは同様のコードの一行目に相当するコードをコンパイルしようとした時点で、次のようなコンパイルエラーが出ます(Anyは全ての型のスーパークラスで、AnyRefに加え、AnyVal(値型)の値も格納できます)。

scala> val arr: Array[Any] = new Array[String](1)
<console>:7: error: type mismatch;
 found   : Array[String]
 required: Array[Any]

このような結果になるのは、Scalaでは配列は非変だからです。静的型付き言語の型安全性とは、コンパイル時により多くのプログラミングエラーを捕捉するものであるとするなら、配列の設計はScalaの方がJavaより型安全であると言えます。

さて、Scalaでは型パラメータを共変にした時点で、安全ではない操作はコンパイラがエラーを出してくれるので安心ですが、共変をどのような場合に使えるかを知っておくのは意味があります。たとえば、先ほど作成したクラスPair[T1, T2]について考えてみましょう。Pair[T1, T2]は一度インスタンス化したら、変更する操作ができませんから、ArrayStoreExceptionのような例外は起こり得ません。実際、Pair[T1, T2]は安全に共変にできるクラスで、class Pair[+T1, +T2]のようにしても問題が起きません。

scala> class Pair[+T1, +T2](val t1: T1, val t2: T2) {
     |   override def toString(): String = "(" + t1 + "," + t2 + ")"
     | }
defined class Pair

scala> val pair: Pair[AnyRef, AnyRef] = new Pair[String, String]("foo", "bar")
pair: Pair[AnyRef,AnyRef] = (foo,bar)

ここで、Pairは作成時に値を与えたら後は変更できず、したがってArrayStoreExceptionのような例外が発生する余地がないことがわかります。一般的には、一度作成したら変更できない(immutable)などの型パラメータは共変にしても多くの場合問題がありません。

immutableだったら、共変にしても問題は起こらないということか。
利用シーンがまだ思い浮かばない。

演習問題

次のimmutableなStack型の定義(途中)があります。???の箇所を埋めて、Stackの定義を完成させなさい。なお、E >: Tは、EはTの継承元である、という制約を表しています。また、Nothingは全ての型のサブクラスであるような型を表現します。Stack[T]は共変なので、Stack[Nothing]はどんな型のStack変数にでも格納することができます。

trait Stack[+T] {
  def pop: (T, Stack[T])
  def push[E >: T](e: E): Stack[E]
  def isEmpty: Boolean
}

class NonEmptyStack[+T](private val top: T, private val rest: Stack[T]) extends Stack[T] {
  def push[E >: T](e: E): Stack[E] = ???
  def pop: (T, Stack[T]) = ???
  def isEmpty: Boolean = ???
}

case object EmptyStack extends Stack[Nothing] {
  def pop: Nothing = throw new IllegalArgumentException("empty stack")
  def push[E >: Nothing](e: E): Stack[E] = new NonEmptyStack[E](e, this)
  def isEmpty: Boolean = true
}

object Stack {
  def apply(): Stack[Nothing] = EmptyStack
}

解答を見て納得した感じ。
自力では厳しかったっす。。
型を見てなんとなくは解答できても、よくわかっていないと実感。

・反変(contravariant)

次は共変とちょうど対になる性質である反変です。簡単に定義を示しましょう。反変というのは、型パラメータを持ったクラスG、型パラメータT1とT2があったとき、T1 が T2 を継承しているときにのみ、

val : G[T1] = G[T2]

というような代入が許される性質を表します。Scalaでは、クラス定義時に

class G[-T]

のように型パラメータの前に-を付けるとその型パラメータは(あるいはそのクラスは)反変になります。

反変の例として最もわかりやすいものの1つが関数の型です。たとえば、型T1とT2があったとき、

val x1: T1 => AnyRef = T2 => AnyRef型の値
x1(T1型の値)

というプログラムの断片が成功するためには、T1がT2を継承する必要があります。その逆では駄目です。仮に、T1 = String, T2 = AnyRef として考えてみましょう。

val x1: String => AnyRef = AnyRef => AnyRef型の値
x1(String型の値)

ここでx1に実際に入っているのはAnyRef => AnyRef型の値であるため、引数としてString型の値を与えても、AnyRef型の引数にString型の値を与えるのと同様であり、問題なく成功します。T1とT2が逆で、T1 = AnyRef, T2 = Stringの場合、String型の引数にAnyRef型の値を与えるのと同様になってしまうので、これはx1へ値を代入する時点でコンパイルエラーになるべきであり、実際にコンパイルエラーになります。

実際にREPLで試してみましょう。

scala> val x1: AnyRef => AnyRef = (x: String) => (x:AnyRef)
<console>:7: error: type mismatch;
 found   : String => AnyRef
 required: AnyRef => AnyRef
       val x1: AnyRef => AnyRef = (x: String) => (x:AnyRef)
                                              ^

scala> val x1: String => AnyRef = (x: AnyRef) => x
x1: String => AnyRef = <function1>

このように、先ほど述べたような結果になっています。

反変という概念は初めて聞いたけど、こっちは制限をかけるというイメージで使えるので、使いどころはいつかはあるかもという印象。

【型パラメータの境界(bounds)】

型パラメータTに対して何も指定しない場合、その型パラメータTは、どんな型でも入り得ることしかわかりません。そのため、何も指定しない型パラメータTに対して呼び出せるメソッドはAnyに対するもののみになります。しかし、たとえば、順序がある要素からなるリストをソートしたい場合など、Tに対して制約を書けると便利な場合があります。そのような場合に使えるのが、型パラメータの境界(bounds)です。型パラメータの境界には2種類あります。

・上限境界(upper bounds)

1つ目は、型パラメータがどのような型を継承しているかを指定する上限境界(upper bounds)です。上限境界では、型パラメータの後に、

abstract class Show {
  def show: String
}
class ShowablePair[T1 <: Show, T2 <: Show](val t1: T1, val t2: T2) extends Show {
  override def show: String = "(" + t1.show + "," + t2.show + ")"
}

ここで、型パラメータT1、T2ともに上限境界としてShowが指定されているため、t1とt2に対してshowを呼び出すことができます。なお、上限境界を明示的に指定しなかった場合、Anyが指定されたものとみなされます。

うん、ここは記法が違うだけで、Javaと同じイメージ。
Javaはextendsキーワードを使う。

・下限境界(lower bounds)

2つ目は、型パラメータがどのような型のスーパータイプであるかを指定する下限境界(lower bounds)です。下限境界は、共変パラメータと共に用いることが多い機能です。実際に例を見ます。

まず、共変の練習問題であったような、イミュータブルなStackクラスを定義します。このStackは共変にしたいとします。

abstract class Stack[+E]{
  def push(element: E): Stack[E]
  def top: E
  def pop: Stack[E]
  def isEmpty: Boolean
}

しかし、この定義は、以下のようなコンパイルエラーになります。

error: covariant type E occurs in contravariant position in type E of value element
         def push(element: E): Stack[E]
                           ^

このコンパイルエラーは、共変な型パラメータEが反変な位置(反変な型パラメータが出現できる箇所)に出現したということを言っています。一般に、引数の位置に共変型パラメータEの値が来た場合、型安全性が壊れる可能性があるため、このようなエラーが出ます。しかし、このStackは配列と違ってイミュータブルであるため、本来ならば型安全性上の問題は起きません。この問題に対処するために型パラメータの下限境界を使うことができます。型パラメータFをpushに追加し、その下限境界として、Stack の型パラメータEを指定します。

abstract class Stack[+E]{
  def push[F >: E](element: F): Stack[F]
  def top: E
  def pop: Stack[E]
  def isEmpty: Boolean
}

このようにすることによって、コンパイラは、StackにはEの任意のスーパータイプの値が入れられる可能性があることがわかるようになります。そして、型パラメータFは共変ではないため、どこに出現しても構いません。このようにして、下限境界を利用して、型安全な Stackと共変性を両立することができます。

>:で下限境界は設定できるのか。
うん、ここは記法が違うだけで、Javaと同じイメージ。
Javaはsuperキーワードを使う。

Scalaではなぜワイルドカード指定がないのだろうと思ったけど、それ相当のことが今まで紹介したものでできるのかな?
以下の記事の中で解説あり
itpro.nikkeibp.co.jp

ワイルドカードは,型を使う側に付加するものであり,共変/反変な型の変数を宣言する必要があるたびに記述しなければならないため,煩雑であるという欠点がありますが,一方で,型の定義時点では共変/反変でないものを共変/反変であるものとして扱うことができるという利点もあります。

Scalaでも,Existential Typeという機能を使うことで,ワイルドカードと同じことを実現することができます。例えば,上記のワイルドカードを使ったJavaのコードと同じことは,次のようなScalaのコードによって実現できます。

//java.util.List[_ <: Any]と略記することもできる
var s1: java.util.List[String]  = new java.util.ArrayList
var s2: java.util.List[T] forSome { type T <: Any } = s1 
//java.util.List[_ >: String]と略記することもできる
var s3: java.util.List[Any] = new java.util.ArrayList
var s4: java.util.List[T] forSome { type T >: String} = s3

型システム、奥が深いでござる。

他の参考記事
Scalaのジェネリックスを学ぶ - じゅんいち☆かとうの技術日誌



ちなみに、Golangはクラスではなく、すべてInterfaceや構造体で継承のようなことをやるので、今回は全体的に記述なし。


Programming in Scala: Updated for Scala 2.12

Programming in Scala: Updated for Scala 2.12

Scalaパズル 36の罠から学ぶベストプラクティス

Scalaパズル 36の罠から学ぶベストプラクティス

SCALAプログラミング入門

SCALAプログラミング入門

ドワンゴの新卒エンジニア向けの研修資料でScalaに入門 その5(トレイト)

関数型のパラダイムを学んで業務に活かそうということで、以下のドワンゴオリジナルの新卒エンジニア向けの研修資料でScalaを本格的に勉強してみることにした。
dwango.github.io
※Java8でごにょごにょしないのは、Scalaを趣味でやりたいという意図があるだけです。

どうせなら、Scalaの言語仕様からガッツリ学んで自分のものにしたい。
また、可能な限りGolangとの比較も入れていきます。

学んだことをとにかく走り書きしていきます。
【トレイト】
・トレイトの基本
気になった部分を以下に記載

Scalaのトレイトはクラスに比べて以下のような特徴があります。

・複数のトレイトを1つのクラスやトレイトにミックスインできる
・直接インスタンス化できない
・クラスパラメータ(コンストラクタの引数)を取ることができない
以下、それぞれの特徴の紹介をしていきます。

トレイトの必要性については、後述してあるかもしれないが、以下の書籍ではこう記載してある。

クラスには 2 つの相反する役割があります。
1 つ目は「インスタンスを作るためのもの」という役割で、このためには「完結した、必要なものを全部持った、大きなクラス」である必要があります。
2 つ目は「再利用の単位」という役割で、このためには「機能ごとの、余計な物を持っていない、小さな クラス」である必要があります。
クラスが「インスタンスを作るためのもの」として使われているときには、 再利用の単位としては大きすぎるのです。
それならば、再利用の単位とい う役割に特化した、もっと小さい構造(トレイト=メソッドの束)を作ればよいのではないか?──これがトレイトの考え方です。
再利用の単位をクラスと別に作るという点では、Ruby のモジュールに似ていますね。

Rubyのモジュールとの違いは、Ruby1.9.3では、後からincludeしたモジュールはで既存のメソッドが上書きされる中で、トレイトは順番を変えても挙動が変わらないうえに、名前衝突が起こった際には明示的にエラーが発生します。

・複数のトレイトを1つのクラスやトレイトにミックスインできる

Scalaのトレイトはクラスとは違い、複数のトレイトを1つのクラスやトレイトにミックスインすることができます。

trait TraitA

trait TraitB

class ClassA

class ClassB

// コンパイルできる
class ClassC extends ClassA with TraitA with TraitB

scala> // コンパイルエラー!
     | class ClassD extends ClassA with ClassB
<console>:15: error: class ClassB needs to be a trait to be mixed in
       class ClassD extends ClassA with ClassB

上記の例ではClassAとTraitAとTraitBを継承したClassCを作ることはできますがClassAとClassBを継承したClassDは作ることができません。「class ClassB needs to be a trait to be mixed in」というエラーメッセージが出ますが、これは「ClassBをミックスインさせるためにはトレイトにする必要がある」という意味です。複数のクラスを継承させたい場合はクラスをトレイトにしましょう。

うん、ここは問題なし。
クラスを多重継承しようとした時に、コンパイルエラーが出るということで。

・直接インスタンス化できない
Scalaのトレイトはクラスと違い、直接インスタンス化できません。

scala> trait TraitA
defined trait TraitA
scala> object ObjectA {
     |   // コンパイルエラー!
     |   val a = new TraitA
     | }
<console>:15: error: trait TraitA is abstract; cannot be instantiated
         val a = new TraitA

この制限は回避する方法がいくつかあります。1つはインスタンス化できるようにトレイトを継承したクラスを作ることです。もう1つはトレイトに実装を与えてインスタンス化する方法です。

trait TraitA

class ClassA extends TraitA

object ObjectA {
  // クラスにすればインスタンス化できる
  val a = new ClassA

  // 実装を与えてもインスタンス化できる
  val a2 = new TraitA {}
}

このように実際使う上では、あまり問題にならない制限でしょう。

うん、ここは、実装を与えてもインスタンス化できるというところが、まだピンとこない。
このコード上では、実装を与えているということになるのか??
{}で実装を与えているという解釈で良いのかな。
空メソッド?

・クラスパラメータ(コンストラクタの引数)を取ることができない
Scalaのトレイトはクラスと違いパラメータ(コンストラクタの引数)を取ることができないという制限があります1。

// 正しいプログラム
class ClassA(name: String) {
  def printName() = println(name)
}
scala> // コンパイルエラー!
     | trait TraitA(name: String)
<console>:3: error: traits or objects may not have parameters
trait TraitA(name: String)

これもあまり問題になることはありません。トレイトに抽象メンバーを持たせることで値を渡すことができます。インスタンス化できない問題のときと同じようにクラスに継承させたり、インスタンス化のときに抽象メンバーを実装をすることでトレイトに値を渡すことができます。

trait TraitA {
  val name: String
  def printName(): Unit = println(name)
}

// クラスにして name を上書きする
class ClassA(val name: String) extends TraitA

object ObjectA {
  val a = new ClassA("dwango")

  // name を上書きするような実装を与えてもよい
  val a2 = new TraitA { val name = "kadokawa" }
}

以上のようにトレイトの制限は実用上ほとんど問題にならないようなものであり、その他の点ではクラスと同じように使うことができます。つまり実質的に多重継承と同じようなことができるわけです。そしてトレイトのミックスインはモジュラリティに大きな恩恵をもたらします。是非使いこなせるようになりましょう。

大まかにトレイトの基本的な使い方はこれで大丈夫そう。

・菱形継承問題
Scalaではoverride指定なしの場合メソッド定義の衝突はエラーになるということだけ理解できればOK。
ただ、Trait同士でメソッド定義がぶつかった時に、overrideとトレイト名の指定で呼び出せることだけは覚えておく。
上記のように、複数継承したものを全てを明示的に呼ぶ方法として、線形化(linearization)」という機能があるそう。

・線形化(linearization)

Scalaのトレイトの線形化機能とは、トレイトがミックスインされた順番をトレイトの継承順番と見做すことです。

次に以下の例を考えてみます。先程の例との違いはTraitBとTraitCのgreetメソッド定義にoverride修飾子が付いていることです。

trait TraitA {
  def greet(): Unit
}

trait TraitB extends TraitA {
  override def greet(): Unit = println("Good morning!")
}

trait TraitC extends TraitA {
  override def greet(): Unit = println("Good evening!")
}

class ClassA extends TraitB with TraitC

この場合はコンパイルエラーにはなりません。ではClassAのgreetメソッドを呼び出した場合、いったい何が表示されるのでしょうか?実際に実行してみましょう。

scala> (new ClassA).greet()
Good evening!

ClassAのgreetメソッドの呼び出しで、TraitCのgreetメソッドが実行されました。これはトレイトの継承順番が線形化されて、後からミックスインしたTraitCが優先されているためです。つまりトレイトのミックスインの順番を逆にするとTraitBが優先されるようになります。以下のようにミックスインの順番を変えてみます

class ClassB extends TraitC with TraitB

するとClassBのgreetメソッドの呼び出して、今度はTraitBのgreetメソッドが実行されます。

scala> (new ClassB).greet()
Good morning!

superを使うことで線形化された親トレイトを使うこともできます

trait TraitA {
  def greet(): Unit = println("Hello!")
}

trait TraitB extends TraitA {
  override def greet(): Unit = {
    super.greet()
    println("My name is Terebi-chan.")
  }
}

trait TraitC extends TraitA {
  override def greet(): Unit = {
    super.greet()
    println("I like niconico.")
  }
}

class ClassA extends TraitB with TraitC
class ClassB extends TraitC with TraitB

このgreetメソッドの結果もまた継承された順番で変わります。

scala> (new ClassA).greet()
Hello!
My name is Terebi-chan.
I like niconico.

scala> (new ClassB).greet()
Hello!
I like niconico.
My name is Terebi-chan.

線形化の機能によりミックスインされたすべてのトレイトの処理を簡単に呼び出せるようになりました。このような線形化によるトレイトの積み重ねの処理をScalaの用語では積み重ね可能なトレイト(Stackable Trait)と呼ぶことがあります。

この線形化がScalaの菱形継承問題に対する対処法になるわけです。

overrideしたメソッドの中でsuperを使用して、新たに定義したい内容を記述する。
このような設計にならないようにはするが、やり方は覚えておこう。

・abstract override

通常のメソッドのオーバーライドでsuperを使ってスーパークラスのメソッドを呼びだす場合、当然のことながら継承元のスーパークラスにそのメソッドの実装がなければならないわけですが、 Scalaには継承元のスーパークラスにそのメソッドの実装がない場合でもメソッドのオーバーライドが可能なabstract overrideという機能があります。

abstract overrideではないoverrideとabstract overrideを比較してみましょう。

trait TraitA {
  def greet(): Unit
}
scala> // コンパイルエラー!
     | trait TraitB extends TraitA {
     |   override def greet(): Unit = {
     |     super.greet()
     |     println("Good morning!")
     |   }
     | }
<console>:16: error: method greet in trait TraitA is accessed from super. It may not be abstract unless it is overridden by a member declared `abstract' and `override'
           super.greet()
// コンパイルできる
trait TraitC extends TraitA {
  abstract override def greet(): Unit = {
    super.greet()
    println("Good evening!")
  }
}

abstract修飾子を付けていないTraitBはコンパイルエラーになってしまいました。エラーメッセージの意味は、TraitAのgreetメソッドには実装がないのでabstract overrideを付けない場合オーバーライドが許されないということです。

オーバーライドをabstract overrideにすることでスーパークラスのメソッドの実装がない場合でもオーバーライドすることができます。この特性は抽象クラスに対しても積み重ねの処理が書けるということを意味します。

しかしabstract overrideでも1つ制約があり、ミックスインされてクラスが作られるときにはスーパークラスのメソッドが実装されてなければなりません。

trait TraitA {
  def greet(): Unit
}

trait TraitB extends TraitA {
  def greet(): Unit =
    println("Hello!")
}

trait TraitC extends TraitA {
  abstract override def greet(): Unit = {
    super.greet()
    println("I like niconico.")
  }
}
scala> // コンパイルエラー!
     | class ClassA extends TraitC
<console>:15: error: class ClassA needs to be a mixin, since method greet in trait TraitC of type ()Unit is marked `abstract' and `override', but no concrete implementation could be found in a base class
       class ClassA extends TraitC
// コンパイルできる
class ClassB extends TraitB with TraitC

abstract overrideの制約のミックスインされてクラスが作られるときにはスーパークラスのメソッドが実装されてならないということだけ、覚えておこう。(当たり前といえば、当たり前だが)

・自分型

Scalaにはクラスやトレイトの中で自分自身の型にアノテーションを記述することができる機能があります。これを自分型アノテーション(self type annotations)や単に自分型(self types)などと呼びます。

trait Greeter {
  def greet(): Unit
}

trait Robot {
  self: Greeter =>

  def start(): Unit = greet()
  override final def toString = "Robot"
}

このロボット(Robot)は起動(start)するときに挨拶(greet)するようです。 Robotは直接Greeterを継承していないのにもかかわらずgreetメソッドを使えていることに注意してください。

このロボットのオブジェクトを実際に作るためにはgreetメソッドを実装したトレイトが必要になります。 REPLを使って動作を確認してみましょう。

scala> trait HelloGreeter extends Greeter {
     |   def greet(): Unit = println("Hello!")
     | }
defined trait HelloGreeter

scala> val r = new Robot with HelloGreeter
r: Robot with HelloGreeter = Robot

scala> r.start()
Hello!

自分型を使う場合は、抽象トレイトを指定し、後から実装を追加するという形になります。このように後から(もしくは外から)利用するモジュールの実装を与えることを依存性の注入(Dependency Injection)と呼ぶことがあります。自分型を使われている場合、この依存性の注入のパターンが使われていると考えてよいでしょう。

ではこの自分型によるトレイトの指定は以下のように直接継承する場合と比べてどのような違いがあるのでしょうか。

trait Greeter {
  def greet(): Unit
}

trait Robot2 extends Greeter {
  def start(): Unit = greet()
  override final def toString = "Robot2"
}

オブジェクトを生成するという点では変わりません。 Robot2も先程と同じように作成することができます。ただし、このトレイトを利用する側や、継承したトレイトやクラスにはGreeterトレイトの見え方に違いができます。

scala> val r: Robot = new Robot with HelloGreeter
r: Robot = Robot
scala> r.greet()
<console>:17: error: value greet is not a member of Robot
       r.greet()
         ^
scala> val r: Robot2 = new Robot2 with HelloGreeter
r: Robot2 = Robot2

scala> r.greet()
Hello!

継承で作られたRobot2オブジェクトではGreeterトレイトのgreetメソッドを呼び出せてしまいますが、自分型で作られたRobotオブジェクトではgreetメソッドを呼びだすことができません。

Robotが利用を宣言するためにあるGreeterのメソッドが外から呼び出せてしまうことはあまり良いことではありません。この点で自分型を使うメリットがあると言えるでしょう。逆に単に依存性を注入できればよいという場合には、この動作は煩わしく感じられるかもしれません。

テスタビリティ的には、可能な限りDIした方が良いと思うので、自分型は積極的に使っていった方が良いと思う。

もう1つ自分型の特徴としては型の循環参照を許す点です。

自分型を使う場合は以下のようなトレイトの相互参照を許しますが、

trait Robot {
  self: Greeter =>

  def name: String

  def start(): Unit = greet()
}

// コンパイルできる
trait Greeter {
  self: Robot =>

  def greet(): Unit = println(s"My name is $name")
}

これを先ほどのように継承に置き換えることではできません。

scala> trait Robot extends Greeter {
     |   def name: String
     | 
     |   def start(): Unit = greet()
     | }
<console>:16: error: illegal inheritance;
 self-type Robot does not conform to Greeter's selftype Greeter with Robot
       trait Robot extends Greeter {
                           ^

scala> // コンパイルエラー
     | trait Greeter extends Robot {
     |   def greet(): Unit = println(s"My name is $name")
     | }
<console>:15: error: illegal inheritance;
 self-type Greeter does not conform to Robot's selftype Robot with Greeter
       trait Greeter extends Robot {

しかし、このように循環するような型構成を有効に使うのは難しいかもしれません。

依存性の注入を使う場合、継承を使うか、自分型を使うかというのは若干悩ましい問題かもしれません。機能的には継承があればよいと言えますが、上記のような可視性の問題がありますし、自分型を使うことで依存性の注入を利用しているとわかりやすくなる効果もあります。利用する場合はチームで相談するとよいかもしれません。

型の循環参照はやめておいたほうが良いという印象。
ここはこういうことができるよという紹介かな。

・落とし穴:トレイトの初期化順序

Scalaのトレイトのvalの初期化順序はトレイトを使う上で大きな落とし穴になります。以下のような例を考えてみましょう。トレイトAで変数fooを宣言し、トレイトBがfooを使って変数barを作成し、クラスCでfooに値を代入してからbarを使っています。

trait A {
  val foo: String
}

trait B extends A {
  val bar = foo + "World"
}

class C extends B {
  val foo = "Hello"

  def printBar(): Unit = println(bar)
}

REPLでクラスCのprintBarメソッドを呼び出してみましょう。

scala> (new C).printBar()
nullWorld

nullWorldと表示されてしまいました。クラスCでfooに代入した値が反映されていないようです。どうしてこのようなことが起きるかというと、Scalaのクラスおよびトレイトはスーパークラスから順番に初期化されるからです。この例で言えば、クラスCはトレイトBを継承し、トレイトBはトレイトAを継承しています。つまり初期化はトレイトAが一番先におこなわれ、変数fooが宣言され、中身は何も代入されていないので、nullになります。次にトレイトBで変数barが宣言されnullであるfooと"World"という文字列から"nullWorld"という文字列が作られ、変数barに代入されます。先ほど表示された文字列はこれになります。

このような簡単な例なら気づきやすいのですが、似たような形の大規模な例もあります。先ほど自分型で紹介した「依存性の注入」は、上位のトレイトで宣言したものを、中間のトレイトで使い、最終的にインスタンス化するときにミックスインするという手法です。ここでもうっかりすると同じような罠を踏んでしまいます。 Scala上級者でもやってしまうのがvalの初期化順の罠なのです。

これは絶対どこかでやってしまいそう。
Scalaスーパークラスから順番に初期化されることは覚えておこう。

・トレイトのvalの初期化順序の回避方法

では、この罠はどうやれば回避できるのでしょうか。上記の例で言えば、使う前にちゃんとfooが初期化されるように、barの初期化を遅延させることです。処理を遅延させるにはlazy valかdefを使います。

具体的なコードを見てみましょう。

trait A {
  val foo: String
}

trait B extends A {
  lazy val bar = foo + "World" // もしくは def bar でもよい
}

class C extends B {
  val foo = "Hello"

  def printBar(): Unit = println(bar)
}

先ほどのnullWorldが表示されてしまった例と違い、barの初期化にlazy valが使われるようになりました。これによりbarの初期化が実際に使われるまで遅延されることになります。その間にクラスCでfooが初期化されることにより、初期化前のfooが使われることがなくなるわけです。

今度はクラスCのprintBarメソッドを呼び出してもちゃんとHelloWorldと表示されます。

scala> (new C).printBar()
HelloWorld

lazy valはvalに比べて若干処理が重く、複雑な呼び出しでデッドロックが発生する場合があります。 valのかわりにdefを使うと毎回値を計算してしまうという問題があります。しかし、両方とも大きな問題にならない場合が多いので、特にvalの値を使ってvalの値を作り出すような場合はlazy valかdefを使うことを検討しましょう。

トレイトのvalの初期化順序を回避するもう1つの方法としては事前定義(Early Definitions)を使う方法もあります。事前定義というのはフィールドの初期化をスーパークラスより先におこなう方法です。

trait A {
  val foo: String
}

trait B extends A {
  val bar = foo + "World" // valのままでよい
}

class C extends {
  val foo = "Hello" // スーパークラスの初期化の前に呼び出される
} with B {
  def printBar(): Unit = println(bar)
}

上記のCのprintBarを呼び出しても正しくHelloWorldと表示されます。

この事前定義は利用側からの回避方法ですが、この例の場合はトレイトBのほうに問題がある(普通に使うと初期化の問題が発生してしまう)ので、トレイトBのほうを修正したほうがいいかもしれません。

トレイトの初期化問題は継承されるトレイト側で解決したほうが良いことが多いので、この事前定義の機能は実際のコードではあまり見ることはないかもしれません。

トレイトの初期化問題、lazy valあるいはdefで解決するとのこと。
事前定義などと裏技もあるのか。
デッドロック問題も頭に留めておこう。


ちなみに、Golangはクラスではなく、すべてInterfaceや構造体で継承のようなことをやるので、今回は全体的に記述なし。


Programming in Scala: Updated for Scala 2.12

Programming in Scala: Updated for Scala 2.12

Scalaパズル 36の罠から学ぶベストプラクティス

Scalaパズル 36の罠から学ぶベストプラクティス

SCALAプログラミング入門

SCALAプログラミング入門

ドワンゴの新卒エンジニア向けの研修資料でScalaに入門 その4(オブジェクト)

関数型のパラダイムを学んで業務に活かそうということで、以下のドワンゴオリジナルの新卒エンジニア向けの研修資料でScalaを本格的に勉強してみることにした。
dwango.github.io
※Java8でごにょごにょしないのは、Scalaを趣味でやりたいという意図があるだけです。

どうせなら、Scalaの言語仕様からガッツリ学んで自分のものにしたい。
また、可能な限りGolangとの比較も入れていきます。

学んだことをとにかく走り書きしていきます。
【オブジェクト】
・オブジェクトに関して

Scalaでは、全ての値がオブジェクトです。また、全てのメソッドは何らかのオブジェクトに所属しています。そのため、Javaのようにクラスに属するstaticフィールドやstaticメソッドといったものを作成することができません。その代わりというと若干語弊があるのですが、objectキーワードによって、同じ名前のシングルトンオブジェクトをグローバルな名前空間に1つ定義することができます。objectキーワードによって定義したオブジェクトもオブジェクトであるため、メソッドやフィールドを定義することができます。

全ての値がオブジェクトであるのは、Rubyなどと一緒なので、よしとするが、staticフィールドやstaticメソッドが作成できないのか。
staticの代わり?にobjectキーワードで作成されたシングルトンオブジェクト内に定義するイメージか。
・object構文の主な用途

object構文の主な用途としては、

・ユーティリティメソッドやグローバルな状態の置き場所(Javaで言うstaticメソッドやフィールド)
・オブジェクトのファクトリメソッド
・Singletonパターン
3つが挙げられます。とはいえ、Singletonパターンを実現するために使われることはほとんどなく、もっぱら最初の 2つの用途で使われます。

objectの基本構文はクラスとほとんど同じで、

object オブジェクト名 extends クラス名 with トレイト名1 with トレイト名2 ... {
  本体
}

のようになります。Scalaでは標準でPredefというobjectが定義・インポートされており、これは最初の使い方に当てはまります。たとえば、println()などとなにげなく使っていたメソッドも実はPredef objectのメソッドなのです。

Object構文の主な用途が3つというのはなんとなくイメージできた。
Predefオブジェクトの話は、コップ本に書いていたから理解済み。

一方、2番めの使い方について考えてみます。点を表すPointクラスのファクトリを objectで作ろうとすると、次のようになります。applyという名前のメソッドはScala処理系によって特別に扱われ、Point(x)のような記述があった場合で、Point objectにapplyという名前のメソッドが定義されていた場合、Point.apply(x)と解釈されます。これを利用してPoint objectの applyメソッドでオブジェクトを生成するようにすることで、Point(3, 5)のような記述でオブジェクトを生成できるようになります。

scala> class Point(val x:Int, val y:Int)
defined class Point

scala> object Point {
     |   def apply(x: Int, y: Int): Point = new Point(x, y)
     | }
defined object Point
warning: previously defined class Point is not a companion to object Point.
Companions must be defined together; you may wish to use :paste mode for this.

これは、new Point()で直接Pointオブジェクトを生成するのに比べて、
・クラス(Point)の実装詳細を内部に隠しておける(インタフェースのみを外部に公開する)
・Pointではなく、そのサブクラスのインスタンスを返すことができる
といったメリットがあります。

サブクラスのインスタンスを返すことができるというのが、イマイチまだピンと来ていないが、他は大まかに理解。

なお、上記の記述はケースクラスを用いてもっと簡単に

scala> case class Point(x: Int, y: Int)
defined class Point

と書けます。ケースクラスは後述するパターンマッチのところでも出てきますが、ここではその使い方については触れません。簡単に言うとケースクラスは、それをつけたクラスのプライマリコンストラクタ全てのフィールドを公開し、equals()・hashCode()・toString()などのオブジェクトの基本的なメソッドを持ったクラスを生成し、また、そのクラスのインスタンスを生成するためのファクトリメソッドを生成するものです。たとえば、 case class Point(x: Int, y: Int)で定義した Point クラスは equals() メソッドを明示的に定義してはいませんが、

Point(1, 2).equals(Point(1, 2))

を評価した値はtrueになります。

ケースクラスにした場合は、Javaでいう自分でクラスを作成した時に作るequals、hashCode、toStringを作ってくれるのか。
これは便利。
フィールドが公開となっているのは、case classでmatch文を使う時にgetter/setterを書いていると冗長になるからかな。

・コンパニオンオブジェクトに関して

クラス名と同じ名前のシングルトンオブジェクトはコンパニオンオブジェクトと呼ばれます。コンパニオンオブジェクトは対応するクラスに対して特権的なアクセス権を持っています。たとえば、 weightをprivateにした場合、

class Person(name: String, age: Int, private val weight: Int)

object Hoge {
  val taro = new Person("Taro", 20, 70)
  println(taro.weight)
}

はNGですが、

class Person(name: String, age: Int, private val weight: Int)

object Person {
  val taro = new Person("Taro", 20, 70)
  println(taro.weight)
}

はOKです。なお、コンパニオンオブジェクトでも、private[this](そのオブジェクト内からのみアクセス可能)なクラスのメンバーに対してはアクセスできません。単にprivateとした場合、コンパニオンオブジェクトからアクセスできるようになります。

class Point(val x: Int, val y: Int) {
  def +(p: Point): Point = {
    new Point(x + p.x, y + p.y)
  }
  override def toString(): String = "(" + x + ", " + y + ")"
}

上記のような感じでメソッド定義の中から直接コンストラクタ引数を参照できるそうです。

ドワンゴの新卒エンジニア向けの研修資料でScalaに入門 その3(クラス)

関数型のパラダイムを学んで業務に活かそうということで、以下のドワンゴオリジナルの新卒エンジニア向けの研修資料でScalaを本格的に勉強してみることにした。
dwango.github.io
※Java8でごにょごにょしないのは、Scalaを趣味でやりたいという意図があるだけです。

どうせなら、Scalaの言語仕様からガッツリ学んで自分のものにしたい。
また、可能な限りGolangとの比較も入れていきます。

学んだことをとにかく走り書きしていきます。
【クラス】
・クラス定義
気になった部分を以下に記載

まず、最初の点ですが、Scalaでは1クラスに付き、基本的には1つのコンストラクタしか使いません。文法上は複数のコンストラクタを定義できるようになっていますが、実際に使うことはまずないので覚える必要はないでしょう。一応、Scalaでは複数のコンストラクタが定義できるので、この最初の1つのコンストラクタをプライマリコンストラクタとして特別に扱っています。

確かにコップ本では複数のコンストラクタを定義する例は書いてたけど、実際には使うことはまずないのか。
なぜかは置いておき、一旦そういうものだということにしておこう。

プライマリコンストラクタの引数にval/varをつけるとそのフィールドは公開され、外部からアクセスできるようになります。なお、コンストラクタ引数のスコープはクラス定義全体におよびます。

class Point(val x: Int, val y: Int) {
  def +(p: Point): Point = {
    new Point(x + p.x, y + p.y)
  }
  override def toString(): String = "(" + x + ", " + y + ")"
}

上記のような感じでメソッド定義の中から直接コンストラクタ引数を参照できるそうです。

・メソッド定義

先ほど既にメソッド定義の例として+メソッドの定義が出てきましたが、一般的には、

(private[this]/protected[package名]) def メソッド名(引数名1: 引数1の型, 引数名2: 引数2の型, ...): 返り値の型 = {
  本体のコード
}

という形をとります。ちなみに、メソッド本体が1つの式だけからなる場合は、

(private[this]/protected[package名]) def メソッド名(引数名1: 引数1の型, 引数名2: 引数2の型, ...): 返り値の型 = 本体のコード

と書けます(実際には、こちらの方が基本形で、= {}を使ったスタイルの方が{}内に複数の式を並べて書けることを利用した派生形になりますが、前者のパターンを使うことが多いでしょう)。
返り値の型は省略しても特別な場合以外型推論してくれますが、読みやすさのために、返り値の型は明記する習慣を付けるようにしましょう。
また、privateを付けるとそのクラス内だけから、protectedを付けるとそのクラスの派生クラスからのみアクセスできるフィールドになります。さらに、 private[this] をつけると、同じオブジェクトからのみ、 protected[パッケージ名] をつけると、追加で同じパッケージに所属しているもの全てからアクセスできるようになります。privateもprotectedも付けない場合、そのフィールドはpublicとみなされます。

private[this]という定義方法があるのか。
返り値の明示的に書くということは、いくら型推論してくれるからとはいえ、確かに心がけたいところ。

複数の引数リストを持つメソッドには、Scalaの糖衣構文と組み合わせて流暢なAPIを作ったり、後述するimplicit parameterのために必要になったり、型推論を補助するために使われたりといった用途があります

implicit parameterは名前だけ聞いたことあるが、まだわからん。
デフォルト引数のようなものかな。

何はともあれ、複数の引数リストを持つ加算メソッドを定義してみましょう。

scala> class Adder {
     |   def add(x: Int)(y: Int): Int = x + y
     | }
defined class Adder

scala> val adder = new Adder()
adder: Adder = Adder@7263d8eb

scala> adder.add(2)(3)
res1: Int = 5

scala> adder.add(2) _
res2: Int => Int = <function1>

複数の引数リストを持つメソッドはobj.m(x, y)の形式でなくobj.m(x)(y)の形式で呼びだすことになります。また、一番下の例のように最初の引数だけを適用して新しい関数を作る(部分適用)こともできます。

obj.m(x, y)の形式でなくobj.m(x)(y)の形式で呼びだすのかー。
JavaScriptで学ぶ関数型言語の時に、undersocre.jsでそのような指定の仕方があったような気がする、ちょい違和感ある。
引数を一つだけ渡して、部分適用するのは自分の頭の中では違和感なし。

・フィールド定義
特に言及するポイントなし。

・継承

クラスのもう1つの機能は、継承です。継承には2つの目的があります。 1つは継承によりスーパークラスの実装をサブクラスでも使うことで実装を再利用することです。もう1つは複数のサブクラスが共通のスーパークラスのインタフェースを継承することで処理を共通化することです1。

実装の継承には複数の継承によりメソッドやフィールドの名前が衝突する場合の振舞いなどに問題があることが知られており、Javaでは実装継承が1つだけに限定されています。 Java 8ではインタフェースにデフォルトの実装を持たせられるようになりましたが、変数は持たせられないなどの制約があります。 Scalaではトレイトという仕組みで複数の実装の継承を実現していますが、トレイトについては別の節で説明します。

1. このように継承などにより型に親子関係を作り、複数の型に共通のインタフェースを持たせることをサブタイピング・ポリモーフィズムと呼びます。Scalaでは他にも構造的部分型というサブタイピング・ポリモーフィズムの機能がありますが、実際に使われることが少ないため、このテキストでは説明を省略しています

多重継承問題を取り上げていて、良い資料だなと感じました。

基本的に、継承のはたらきはJavaのクラスと同じですが、既存のメソッドをオーバーライドするときはoverrideキーワードを使わなければならない点が異なります。たとえば、

scala> class APrinter() {
     |   def print(): Unit = {
     |     println("A")
     |   }
     | }
defined class APrinter

scala> class BPrinter() extends APrinter {
     |   override def print(): Unit = {
     |     println("B")
     |   }
     | }
defined class BPrinter

scala> new APrinter().print
A

scala> new BPrinter().print
B

のようにすることができます。ここでoverrideキーワードをはずすと、

scala> class BPrinter() extends APrinter {
     |   def print(): Unit = {
     |     println("B")
     |   }
     | }
<console>:14: error: overriding method print in class APrinter of type ()Unit;
 method print needs `override' modifier
         def print(): Unit = {
             ^

のようにメッセージを出力して、コンパイルエラーになります。Javaではしばしば、気付かずに既存のメソッドをオーバーライドするつもりで新しいメソッドを定義してしまうというミスがありましたが、Scalaではoverrideキーワードを使って言語レベルでこの問題に対処しているのです。

継承時にメソッドをoverrideする時はJavaでやるようなoverrideアノテーションではなく、言語機能としてoverrideキーワードを使用するそうです。
overrideキーワードを使わないとコンパイルエラーになるなんて、良い仕様だな。

ちなみに、Goはクラスではなく、すべてInterfaceや構造体で継承のようなことをやるので、今回は全体的に記述なし。


Programming in Scala: Updated for Scala 2.12

Programming in Scala: Updated for Scala 2.12

Scalaパズル 36の罠から学ぶベストプラクティス

Scalaパズル 36の罠から学ぶベストプラクティス

SCALAプログラミング入門

SCALAプログラミング入門