無限大な夢のあと

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

「エリック・エヴァンズのドメイン駆動設計」を読んで(自分用メモ) 第2部 モデル駆動設計の構成要素(前半 4章-5章)

DDD導入研修の課題として、下記の書籍「エリック・エヴァンズのドメイン駆動設計」を読むことになりました。

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

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

本文章は上記の書籍を引用させて頂きます。


ただ漠然と読んでいるだけだと、頭を通りすぎていきそうなので、自分なりに要点や響いた箇所、メモを残しながら読んでいこうと思います。
「Domain-Drive Design Quickly」は約80ページしかありませんでしたが、こちらはボリュームが大きいので、覚悟しながら読もうと思います。

Kindle書籍からだとコピーできないため、簡単にまとめていきます。

☆第2部 モデル駆動設計の構成要素

・introduction

f:id:noimpslmtbrk:20160915132551p:plain

モデル駆動設計を構成する言語のナビゲーションマップとのことです。
まだ、中身の用語の定義が曖昧なままなので、ここは読んでいく上で何度も立ち戻りたい。


・第4章 ドメインを隔離する。

レイヤ化アーキテクチャ
f:id:noimpslmtbrk:20160915152220p:plain

・ユーザインタフェース(プレゼンテーション層)
 ユーザに情報を表示して、ユーザのコマンドを解釈する責務を負う。外部アクタは人間のユーザではなく、別のコンピュータシステムのこともある。


・アプリケーション層
 ソフト雨ケアが行うことになっている仕事を定義し、表現力豊かなドメインオブジェクトが問題を解決するように導く。このレイヤが責務を負う作業は、ビジネスにとって意味があるものか、あるいは他システムのアプリケーション層と相互作用するのに必要なものである。
 このレイヤは薄く保たれる。
 ビジネスルールや知識を含まず、やるべき作業を調整するだけで、実際の処理は、ドメインオブジェクトによって直下のレイヤで実行される共同作業に移譲する。ビジネスの状況を反映する状態は持たないが、ユーザやプログラムが行う作業の進捗を反映する状態を持つことはできる。


ドメイン
 ビジネスの概念と、ビジネスが置かれた状況に関する情報、およびビジネスルールを表す責務を負う。ビジネスの状況を反映する状態はここで制御され、使用されるが、それを格納するという技術的な詳細は、インフラストラクチャに移譲される。この層がビジネスソフトウェアの核心である。


・インフラストラクチャ層
 上位のレイヤを支える一般的な技術的機能を提供する。これには、アプリケーションのためのメッセージ送信、ドメインのための永続化、ユーザインターフェースのためのウィジェット層などがある。インフラストラクチャ層は、ここで示す4層間における相互作用のパターンも、アーキテクチャフレームワークを通じてサポートすることもある。


プロジェクトによっては、ユーザインタフェースそうとアプリケーション層を厳密に区別しないこともある。また、複数のインフラストラクチャ層を持つこともある。しかし、ドメイン層を分離して初めて、モデル駆動開発が可能になるのだ。

ここは各レイヤの説明と共に残しておく。

複雑なプログラムはレイヤに分割すること。各レイヤで設計を進め、凝集度を高めて下位層にだけに依存するようにすること。
標準的なアーキテクチャーパターンに従って、上位のレイヤに対しては疎結合にすること。
ドメインモデルに関係するコード全部を1つの層に集中させ、ユーザインタフェース、アプリケーション、インフラストラクチャのコードから分離すること。
表示や格納、アプリケーションタスク管理などの責務から解放されることで、ドメインオブジェクトはドメインモデルを表現するという責務に専念できる。これによって、モデルは十分豊かで明確になるように進化し、本質的なビジネスの知識を捉えて、それを機能させることができるようにする。

ここも実際に適用する際のメモとして、残しておく。

レイヤを関係づける

レイヤ同士は疎結合であるべきで、設計の依存関係は1方向にだけ向けられる。
上位のレイヤは下位のレイヤにある要素を直接使用した入り、操作したりできる。

下位のレイヤにあるオブジェクトが上方と通信が必要な場合は、コールバックやオブザーバといったようなレイヤ同士を関係づけるためのアーキテクチャーパターンである。

ここも原理原則ではあるが、メモ。

利口なUI「アンチパターン

アプリケーション処理をドメイン層から分離したくないパターン

■利点
・ 単純なアプリケーションの場合、生産性が高く、すぐに作れる。
・ それほど有能でない開発者でも、この方法ならほとんど訓練しないで仕事ができる。
・ 要求分析が不足していても、プロトタイプをユーザに公開し、その要望を満たすように製品を変更することで、問題を克服できる。
・ アプリケーションが互いに分離しているので、小さなモジュールの納品スケジュールは比較的正確に計画できる。単純な振る舞いを付け加えるようなシステムの拡張であれば、容易に対応できるだろう、
・ 関係データベースはうまく機能し、データレベルでの統合が実現される。
・ 4GL ツールが実にうまく機能する。
・ アプリケーションが引き継がれた場合、保守プログラムは自分が理解できない部分を素早く作り変えられる。変更による影響が、それぞれ特定のユーザインタフェースに限定されるからだ。

■欠点
・ アプリケーションの統合は困難で、データベースを経由させるしかない。
・ 振る舞いが再利用されることも、ビジネスの問題が抽象化されることもない。ビジネスルールは、適用先の操作それぞれで複製されることになる。
・ 迅速なプロトタイピングやイテレーションを行おうとしても、自然と限界に行き当たる。抽象化が欠けているために、リファクタリングの選択肢が制限されるからだ。
・ 複雑さによって、すぐに覆い尽くされてしまうので、成長しようとしても、単純なアプリケーションを追加することしかできない。より豊かな振る舞いが実現できるようになるといった、優雅な道は存在しない

Viewにロジックが混ざったみたいな状況のことを指しているということみたい。
ドメイン駆動設計とは反対の思想で大規模で複雑なものには採用するべきではない。


・第5章 ソフトウェアで表現されたモデル 

あるオブジェクトは、状態が異なったり、さらに別々の実装をまたいだ入りしたとしても追跡されるような、連続性と一意性を持ったものを表現しているのか?
それとも、他の何かの状態を記述する属性なのか?
これがエンティティと値オブジェクトとの基本的な区別である。
なんらかのパターンに従うオブジェクトを定義することで、そのオブジェクトは曖昧ではなくなり、強固な設計を行うための具体的な選択に向かう道筋が整えられる。

エンティティと値オブジェクトの話は、後でも説明は出ると思うけど、ここは押さえておく。

次に、ドメインの側面によっては、オブジェクトとしてよりも、アクションや操作として表現した方が明確になるものもある。
オブジェクト指向モデリングの伝統からやや外れるが、これらについてはサービスで表現して、操作を行う責務をエンティティや値オブジェクトに押し付けない方が適切であることも多い
サービスとは、要求に応じてクライアントのために行われる何かである。ソフトウェアの技術的なレイヤーには多くのサービスがある。また、サービスはドメインにも登場する。その際にモデル化されるのは、ソフトウェアが実行すべきことに対応し、状態には対応しないような活動だ。

まだここはピンとこない。

最後に、モジュールに議論することによって、設計上のあらゆる意志決定は、ドメインについてのなんらかの洞察によって動機付けられないければならないという点を強調する。
高凝集と低結合という考え方は、しばしば技術的な指標と考えられているが、概念そのものにも適用できる。モデル駆動設計においては、モジュールはモデルの一部であり、ドメインにおける概念を反映していなければならない。

ここは一旦メモ。

■関連
関連をもっと扱いやすくするには、少なくとも3つの方法がある。
1.関連をたどる方向を強制する。
2.限定子を付加して、多重度を効果的に減らす。
3.本質的ではない関連を除去する。

ここも一旦メモ。

■エンティティ(参照オブジェクト)

オブジェクトモデリングを行うと、我々はオブジェクトの属性に集中しがちだが、エンティティの根本的な概念は抽象的な連続性である。
この連続性はエンティティのライフサイクルを通じて続き、エンティティが多様な形を取っても変わることがない。

オブジェクトの中には、主要な定義が属性によってなされないものもある。そういうオブジェクトは同一性のつながりを表現するのであり、その同一性は、時間が経っても、異なる形で表現されても変わらない。そういうオブジェクトは属性が異なっていても、他のオブジェクトと一致しなければならないことがある。
また、あるオブジェクトは、同じ属性を持っていたとしても、他のオブジェクトと区別しなければならない。同一性を取り違えるとデータの破損につながりかねない。

同一性が一つのキーワードかな。

他方、モデル中の全てのオブジェクトが、意味のある同一性を持ったエンティティであるとは限らない。この問題を複雑にしているのは、オブジェクト指向言語が、あらゆるオブジェクトに「同一性」の演算を組み込んでいるという事実である。(Javaの=演算子など)
これはメモリ中の位置の比較やその他の仕組みによって実現される。その意味で、全てのオブジェクトインスタンスは同一性を持っている。
例えば、Javaの実行環境を作成するドメインや、リモートオブジェクトをローカルでキャッシュする技術的なフレームワークドメインであれば全てのオブジェクトインスタンスがまさにエンティティになるだろう。
しかし、この同一性の仕組みはほかのアプリケーションドメインでは意味をなさない。
同一性は、エンティティの持つ巧妙な意味のある属性であり、プログラミング言語の持つ自動化された機能は引き継げないのである。

最後の1文だけ頭に残しておく。

あるオブジェクトが属性ではなく、同一性によって識別されるのであれば、モデルでこのオブジェクトを定義する際には、その同一性を第一とすること。クラスの定義をシンプルに保ち、ライフサイクルの連続性と同一性に集中すること。クラスの定義をシンプルに保ち、ライフサイクルの連続性と同一性に集中すること。形式や履歴に関係なく、各オブジェクトを識別する手段を定義すること。オブジェクト同士を突き合わせる際に、属性を用いるよう求めてくる要件には注意すること。各オブジェクトに対して結果が一意となることが保証される操作を定義すること。これは一意であることが保証された記号を添えることで、おそらく実現できる。この識別手段は外部に由来する場合もあれば、システムによってシステムのために作成される任意の識別子の場合もあるが、モデルにおける同値性の区別とは一致しなければならない。モデルは同じものであるとうことが何を意味するかを定義しなければならない。

ライフサイクルの連続性というのが頭に?が浮かんでいる。

・エンティティをモデル化する。
オブジェクトをモデル化する時に、属性について考えるのは自然なことであり、その振る舞いについて考えることにも極めて重要だ。
しかし、エンティティにとって最も基本的な責務は、振る舞いが明確で予測可能になるよう、連続性を確立することである。
これが一番うまくいくのは、余計なものがない状態が保たれている時だ。
属性や振る舞いに集中するよりは、エンティティオブジェクトの定義を最も本質的な特徴にまで削ぎ落とすこと。

連続性を確立することというのが、いまいちピンとこない。
また、同一性のための操作を設計するというのは現実の問題と照らしわせると難しい。


■値オブジェクト(VALUE OBJECTS)

多くのオブジェクトには概念的な同一性がない。そういうオブジェクトは、物事の特徴を記述する。

モデルで最も目立つオブジェクトが、通常はエンティティであり、また各エンティティの同一性を追跡することが非常に重要であることから、あらゆるドメインオブジェクトに同一性を割り当てようと考えるのは自然なことである。実際、フレームワークの中には、あらゆるオブジェクトに一意のIDを割り当てるものもある。

エンティティの同一性を追跡するのは本質的なことだが、それ以外のオブジェクトに同一性を与えてしまうと、システムの性能を損なうことにアンリ、分析作業が増え、さらに、全てのオブジェクトの見た目が同じになってしまうことでモデルが台無しになりかねない。
ソフトウェア設計は、複雑さとの恒常的な戦いである。
特別な処理が必要な場所だけで行われるように、区別しなければならない。
しかし、このオブジェクトのカテゴリを、単に同一性のないものとみなしてしまうと、我々の使えるツールや語彙は大して増えない。
実のところ、これらのオブジェクトには、独自の特徴とモデルに対する独自の意味がある。これは物事を記述するオブジェクトなのだ。

あるオブジェクトが、ドメインにおける記述的な側面を表現し、概念的な同一性を守らない場合、そういうオブジェクトは値オブジェクトと呼ばれる。
値オブジェクトがインスタンス化される際に表現しようとするのは、何であるかだけが問題と成り、誰であるか、あるいはどれであるかは問われないような設計の要素である。

本書の中に例があったので、そこで何とか理解。

値オブジェクトは、しばしば、オブジェクト間のメッセージでパラメータとして渡される。一過性のことも多く、操作のために生成されては破棄される。
また、エンティティ(および他の値オブジェクト)の属性として使用される。人は同一性のあるエンティティとして、モデル化されるかもしれないが、その人の名前は値オブジェクトである。

あるモデル要素について、その属性しか関心の対象とならないのであれば、その要素を値オブジェクトとして分類すること。値オブジェクトに、自分が伝える属性の意味を表現させ、関係した機能を与えること。値オブジェクトを不変なものとして扱うこと。
同一性を与えず、エンティティを維持するために必要となる複雑な設計を避けること。

エンティティか値オブジェクトかを決めるための説明って感じ。

値オブジェクトを可変であることを許可する方が良い場合

・値が頻繁に変化する場合
・オブジェクトの生成や削除が高くつく場合
・置き換えによって(修正ではなく)クラスタリングが妨げられる場合(前述の例で説明)
・値が共有することがあまりない場合、またはクラスタリングを改良するためや他の技術的理由からそういう共有を見合わせる場合

今後、自分で設計するにあたってのメモ。

・値オブジェクトを含む関連を設計する

値オブジェクト同士の双方向の関連は完全に取り除くように試みること

ここはメモ。

■サービス

時には、単純に「物」とはできないこともある。
場合によっては、設計をできる限り明確にしつつ、実践的に進めて行くと、概念的にどのオブジェクトにも属さないような操作が含まれることがある。
強引に決着をつけるのではなく、問題領域にひかれる自然な輪郭に従って、モデルの中に明確にサービスを含めれば良い。

サービスの冒頭の説明

ドメインから生まれる概念の中には、オブジェクトしてモデル化すると不自然なものもある。
こうしたドメインで必要な機能をエンティティや値オブジェクトの責務として押し付けると、モデルに基づくオブジェクトの定義を歪めるか、意味のない不自然なオブジェクトを追加することになる。

サービスとは、モデルにおいて独立したインターフェースとして提供される操作で、エンティティと値オブジェクトのようには状態をカプセル化しない。
サービスは技術的なフレームワークでは一般的なパターンだが、ドメイン層にも適用できる。

サービスという名前は他のオブジェクトとの関係性を強調している。
エンティティや値オブジェクトとは異なり、純粋にクライアントに対して何が実行できるかという観点から定義されるのだ。
サービスは、実体よりも活動、つまり名詞よりも動詞にちなんで命名される傾向がある。

サービスは節度を持って使用すべきで、エンティティと値オブジェクトから全ての振る舞いを奪ってはならない。
しかし、実際に、操作が重要なドメインの概念なのであれば、サービスはモデル駆動設計の自然な一部を形成する。

サービスとはモデルを持たない振る舞いを持つもので、純粋にクライアントに対して何が実行できるかという観点から定義されるもの。

優れたサービスには3つの特徴がある
1. 操作がドメインの概念に関係しており、その概念がエンティティや値オブジェクトの自然な一部ではない。
2.ドメインモデルの他の要素の観点からインターフェースが定義されている。
3.操作に状態がない。

ここで状態がないというのは、どのクライアントでも得てのサービスのインスタンスを使うにあたって、インスタンスの持つ個々の履歴を気にする必要がない。という意味である。
サービスを実行すると、グルーバルにアクセス可能な情報を使用し、そのグローバルな情報を変更することもあるかもしれません。(つまり副作用がある。)
しかし、ほとんどのドメインオブジェクトが持っているような、オブジェクトの内部にあって、自身の振る舞いに影響を与える状態は、サービスには存在しない。

ドメインにおける重要なプロセスや変換処理が、エンティティや値オブジェクトの自然な責務ではない場合、その操作はサービスとして宣言される独立したインターフェースとしてモデルに追加すること。
モデルの言語を用いて、インターフェースを定義し、操作名が必ずユビキタス言語の一部になるようにすること。
サービスには状態を持たせないこと。

うん、何となくわかってきた。

・サービスと隔離されたドメイン
このパターンは、ドメインにおいてそれ自体が重要な意味を持つサービスに焦点を絞っているが、もちろん、サービスはドメイン層だけで使用されるのではない。
ドメイン層に属するサービスを他のレイヤーから区別することと、その区別をはっきり保つように責務を分解することには、注意が必要である。

多くのドメインサービスやアプリケーションサービスは、エンティティと値オブジェクトで構成される集合体の上に構築され、ドメインに本来備わっている能力をまとめあげて、実際に何らかの処理を行うスクリプトのように振る舞う。

ドメイン層だけで使用されるものではないこともメモ。

・粒度
このパターンの議論では、概念をサービスとしてモデル化することで得られる表現力を強調しているが、このパターンはクライアントをエンティティと値オブジェクトから分離する手段としても、ドメイン層のインターフェースの粒度を制御する手段としても価値がある。

中粒度で状態を持たないサービスは、巨大なシステムで再利用しやすい。
これは、重要な機能をシンプルなインターフェースの後ろにカプセル化しているためだ。
また、細粒度のオブジェクトは、分散システムにおいては、非効率的なメッセージングにつながるかもしれない。

前述した通り、細粒度のドメインオブジェクトを用いると、ドメイン層からアプリケーション層は、ドメインオブジェクトの振る舞いが組み合わされる場所だからだ。
極めて詳細なレベルでの相互作用が複雑であるため、結局アプリケーション層で処理されることになり、ドメインの知識がアプリケーションやユーザインタフェースコードに這い出るのを許して、その知識はドメイン層から失われてしまう。
ドメインサービスを慎重に導入すれば、複数のレイヤ間で境界を鮮明に維持できる。
このパターンでは、クライアントの操作や用途の広さよりも、インタフェースの単純さが優先される。
また、このパターンより、巨大なシステムや分散システムでコンポーネントをパッケージングするのに、非常に便利な中粒度の機能が提供される。
そして、サービスが、ドメインの概念を表現するのに最も自然な方法であることもある。

中粒度で定義されるべきって感じで良いかな?
ここは何度も自分で設計して経験していかないと実感していけない。


■モジュール(パッケージ)

モジュールを選択する際には、システムに関する物語を伝え、概念の凝集した集合を含んでいるものを選ぶこと。
こうすることで、モジュール間は低結合になることが多い。

モジュールには、ユビキタス言語の一部になる名前をつけること。
モジュールとその名前はドメインに対する洞察を反映していなければならない

当たり前のことだけど、低結合、高凝集がキーワード。

大量のリファクタリングを行う開発者でも、プロジェクトの初期に考え出したモジュールで満足する傾向にある。

今後、ここを見直すというのも視野に入れていこう。

・インフラストラクチャ駆動パッケージングの落とし穴
ティア化アーキテクチャーでは、モデルオブジェクトの実装が断片化されるかもしれない。

別々のサーバにコードを分散させようという意図が実際にない限り、単一の概念オブジェクトを実装するコードは全て、同一のオブジェクトにならなくても、同一のモジュールにまとめること。

ドメイン層を他のコードから分離するためにパッケージングを使用すること。そうでなければ、ドメインの開発者にできる限り選択の余地を残し、モデルと設計上の選択をサポートするように、ドメインオブジェクトをパッケージングできるようにすること。

ティア化とは、責務が断片化された状態のこと。
中規模Web開発のためのMVC分割とレイヤアーキテクチャ - Qiita


モデリングパラダイム

オブジェクト指向が主流になっているシステムに、非オブジェクトの要素を混ぜ合わせるための経験則を4つあげよう。
・実装パラダイムと対立しないこと
  ドメインに関する別の考えは常にある。パラダイムに合うモデルの概念を見つけること。
ユビキタス言語に頼ること
ツール間に厳格なつながりがない場合でも、言語を一貫して使用すれば、設計の各部分が分かれて行ってしまうことはない。
UMLにこだわらないこと
UML図のようなツールに固執すると、容易に描けるものに合わせてモデルがゆがめられることがある。
 例えば、UMLには制約を表現する機能が確かにあるが、常にそれで十分とは限らない。
 別の作図スタイル(他のパラダイムでは一般的かもしれない)を選んだり、単純な自然言語で記述したりした方が、オブジェクトの特定の見方を示すことを意図された作図スタイルを、無理に適合させるよりは良い。
・懐疑であること
 ツールは本当に相応の働きをしているか?ルールがあるからといって、ルールエンジンを使うオーバーヘッドが必要とは限らない。
 ルールは多少わかりにくくはなるにしても、オブジェクトとして表現できるが、パラダイムを複数持つことで、事態は極めて複雑になるのだ。

ここはメモ。


脳が疲れる泣

続いて、第2部 モデル駆動設計の構成要素(後半 6章-7章) 頑張ります。

アナリシスパターン―再利用可能なオブジェクトモデル (Object Technology Series)

アナリシスパターン―再利用可能なオブジェクトモデル (Object Technology Series)

  • 作者: マーチンファウラー,Martin Fowler,堀内一,友野晶夫,児玉公信,大脇文雄
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 2002/04
  • メディア: 単行本
  • 購入: 7人 クリック: 89回
  • この商品を含むブログ (70件) を見る
実践ドメイン駆動設計 (Object Oriented SELECTION)

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

ドメイン駆動 (Programmer’s SELECTION)

ドメイン駆動 (Programmer’s SELECTION)

ビジネスパターンによるモデル駆動設計

ビジネスパターンによるモデル駆動設計

「エリック・エヴァンズのドメイン駆動設計」を読んで(自分用メモ) 第1部 ドメインモデルを機能させる

DDD導入研修の課題として、下記の書籍「エリック・エヴァンズのドメイン駆動設計」を読むことになりました。

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

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

本文章は上記の書籍を引用させて頂きます。


ただ漠然と読んでいるだけだと、頭を通りすぎていきそうなので、自分なりに要点や響いた箇所、メモを残しながら読んでいこうと思います。
「Domain-Drive Design Quickly」は約80ページしかありませんでしたが、こちらはボリュームが大きいので、覚悟しながら読もうと思います。

Kindle書籍からだとコピーできないため、簡単にまとめていきます。

■第1部 ドメインモデルを機能させる

・Intordcution

ドメイン駆動設計におけるモデルの有用性
 1.モデルと設計の確信が相互に形成し合う。
 2.モデルは、チームメンバ全員が使用する言語の基盤である。
 3.モデルとは、蒸留された知識である。

ここも一応メモ。
後ろの章を読むと、より理解度が上がる気がするので。

・第1章 知識を噛み砕く

効率的なモデリングの要素
 1.モデルと実装を結びつける。
 2.モデルにもど突いて言語を洗練させる。
 3.知識豊富なモデルを開発する。
 4.モデルを蒸留する。
 5.ブレインストーミングと実験を行う。

ここの手順はメモとして残しておく。

知識豊富な設計

モデルによって捉えられる知識は「名詞を見つける」ことにとどまらない。
ビジネスの活動やルールも、ドメインに含まれるエンティティと同じように、ドメインにとって中心的なのだ。

エンティティや値を超えて、その先に行こうとする、このような動きに伴った時こそ、知識の嚙み砕きは力を発揮できる。
通常、ドメインエキスパートは、自分の頭の中で起きているプロセスがいかに複雑化を意識することなく、仕事をする中でこれのルールを全て調べて矛盾を調整し、常識で考えて隔たりを埋めている。だが、こういうことはソフトウェアにはできない。
ソフトウェアエキスパートと密接に協力する中で、知識を噛み砕くことによって初めて、ルールが明確となり、具体化されて、折り合いがつけられるか、あるいはスコープの対象外とされるのである。

「名詞を見つける」ことにとどまらないということに少しドキッとした。
ドメインエキスパートとともに知識を噛み砕いて、明確化していくということが大事。

・第2章 コミュニケーションと言語の使い方

声に出してモデリングする

モデルを改良する最適な方法の1つは、話して見ることだ。考えらえるモデルのバリエーションから生じる様々な概念を、声に出して構成してみる。荒削りな表現は聞けば、すぐわかる。

ここは例を見て、そうだなと思った。
実践してみよう。

「彼らには抽象的すぎる。」
「オブジェクトがわかっていない。」
「彼らの用語法に従って要求を集めなければならない。」

豊富な知識を持つドメインエキスパートがモデルを理解できないとしたら、モデルに何か問題があるのだ

ここは現場ではよくありそう。
解決策の一つとして、ユビキタス言語を作っていくというのはあるのか。

設計に関する本質的な詳細は、コードにおいて捉えらえる。

同意。

常に覚えていてほしいのは、モデルは図ではない。

モデルを伝える手段の一つが図であるだけ。

ドキュメントはコードや会話での表現を補わななければならない

そうあるべきだと思う。

すでにコードがうまくやっていること、ドキュメントでもやろうとするべきではない

受託開発でよく作るいわゆる詳細設計書かな。
これらは納品物ということで必要なだけで、いわゆる基本設計書相当のドキュメントがWikiなどに残っていればそれで十分。


・第3章 モデルと実装を結びつける

ソフトウェアシステムの一部を設計する際には、紐付けが明らかになるように、ドメインモデルを文字通りの意味で忠実に反映させること。
モデルについて、再検討し、より自然いソフトウェアに実装されるように修正すること。
これはドメインに対するより深い洞察を反映させようとする時にも言える。強固なユビキタス言語を支えることに加えて、ドメインと実装両方の目的に使える単一のモデルを要求すること

ここは繰り返し記述されていることだが、単一のモデルということに着目。

設計で使用する用語法と責務の基礎的な割り当てをモデルから引き出すこと。
コードはモデルの表現となるから、コードに対する変更はモデルに対する変更になるかもしれない。
その影響は、プロジェクトの他の活動全体へと適宜伝わっていかなけければならない。
実装を一分の狂いもなくモデルに結びつけるには、通常、オブジェクト指向プログラミングのようなモデリングパラダイムをサポートする、ソフトウェア開発のためのツールと言語が必要である。

モデルを元に実装も行っていきましょうという話。
実際問題、コード設計に入った時にトランザクション境界で色々考えなければいけないが、そこは設計パターンみたいな解決のアプローチがあるのかな。

第2部 モデル駆動設計の構成要素 へ続く。

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

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

ドメイン駆動 (Programmer’s SELECTION)

ドメイン駆動 (Programmer’s SELECTION)

ビジネスパターンによるモデル駆動設計

ビジネスパターンによるモデル駆動設計

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プログラミング入門