Scalaの基本を学ぶ【その3】〜ケースクラス・パターンマッチ・エラー処理・Implicitの活用〜

Scalaのケースクラス・パターンマッチング・エラー処理(Option, Either, Try)・Implicit(暗黙の型変換・パラメータ)の使い方を徹底解説!サンプルコード付きで詳しく解説します。

動作環境

Scala:2.12

ケースクラスとパターンマッチング

簡単なケースクラスによるデータ型の定義
CやJavaとの列挙型との違いは、
  • 各々のデータは独立してパラメータを持つことができること
パターンマッチングを用いて定義することで、
  1. ノードの種類と構造によって分岐する
  1. ネストしたノードを分解する
  1. ネストしたノードを分解した結果を変数に束縛する
という3つの動作が同時に行える。
ある日の次の曜日を返すメソッドをパターンマッチを用いて定義している例:
実行例:
※ 変数宣言でもパターンマッチングを行うことが可能
変数宣言でパターンマッチングを行なっている例:

部分関数

無名関数とパターンマッチングの2つの機能を組み合わせた、部分関数(PartialFunction)がScalaには存在する
無名関数の具体的なユースケース:
ここで、メソッドは、を引数に取り、になる要素のみを残し、さらに、の結果の値を元にした新しいコレクションを返します。
は、
の部分から自動的に生成され、パターンがマッチするときのみ真()になるように定義される。

エラー処理

Scalaでのエラー処理は
  1. 例外を使う方法
  1. OptionやEitherやTryなどのデータ型を使う方法
の2つの方法があり、状況に応じて使い分けることになる。

エラー処理で実現しなければならないこと

  • 例外安全性
    • 例外が発生してもシステムがダウンしたり、データの不整合などの問題が起きないようにする概念のこと
  • 強い例外安全性
    • 例外が発生した場合、全ての状態が例外発生前に戻らなければならないという概念のこと

例外の問題点

Javaのエラー処理は例外が中心的な役割を担っていて、Scalaでも例外は多く使われる。しかし、例外は便利な反面、以下のような問題もある。
  • 例外と使うとコントロールフローがわかりづらくなる
    • 適切に使えば正常系の処理とエラー処理を分離し、コードの可読性を上げ、エラー処理をまとめる効果があるが、例外のcatch漏れや、例外をcatchしているところで、どこで発生した例外をcatchしているのか判別できないため、コードの修正を阻害する場合もある
  • 例外は非同期プログラミングでは使えない
    • 例外の動作は送出されたらcatchされるまで、コールスタックを遡っていくというもの。そのため、別スレッド・別のイベントループなどで実行される非同期プログラミングでは相容れない
  • 例外は型チェックできない
    • チェック例外を使わない場合、メソッドの型として表現されない。(Scalaでは、高階関数でチェック例外を扱うことが難しいという問題が大きく、チェック例外の機能は無くなっている)また、間違った例外をキャッチしているかは、実行時にしか分からない。

Option

OptionはScalaで多用されるデータ型の1つ。Option型には、以下の2つの値が存在する。
に具体的な値が入っている場合の実行例:
が入っている場合の実行例:

Optionのパターンマッチ

Optionは型を持っているため、パターンマッチを使って処理することもできる。
実行例:
上記のようにSomeかNoneにパターンマッチすることで、Someにパターンマッチする場合には、その中身の値をという別の変数に束縛することが可能。

Optionの入れ子を解消する

キャッシュから情報を取得する場合、キャッシュヒットする場合と、キャッシュミスする場合があり、それらは、ScalaではOption型で表現されることが多い。以下のようなキャッシュ取得が連続で繰り返された場合、Optionが入れ子になってしまう。
1つ目と2つ目の整数の値がOptionで返ってきて、それをかけた値を求める実行例:
Optionの入れ子を解消するためには、を実行すれば良い。
を実行し、Optionの入れ子を解消している例:
上記のように、v2がNoneである場合でも、は成立する。そのため、キャッシュミスでSomeの値が取れなかった際も問題なく処理が動く。

flatMap

は、実際はこの両方を組み合わせて使うことが多い。は、Optionにをかけてを適用してくれるメソッド

Either

Eitherは、エラー時にエラーの種類まで取得できる。Optionでは、Noneが返ってきた場合、値が取得できなかったことはわかるがエラーの状態は取得できないので、エラーの種類がわからなくても問題にならない場合のみ使える。具体的には、Optionはの2つの値を持つが、Eitherはの2つの値を持つ。

Try

TryはEitherと同じように正常な値とエラー値のどちらかを表現するデータ型。Eitherとの違いは、2つの型が平等ではなく、エラー値がThrowableに限定されており、型引数を1つしか取らないこと。具体的には、Tryはの2つの値を持つ。
は型変数を取り任意の値を入れることができるが、はThrowableしか入れることができない。そして、Tryには、コンパニオンオブジェクトのapplyで生成する際に、例外をcatchしFailureにする機能がある。
Tryの実行例:
この機能を使って、例外が起こりそうな箇所をで包み、にして値として扱えるようにするのがTryの特徴

の例外

がcatchするのは全ての例外ではなく、NonFatalという種類の例外のみ。その理由は、NonFatalでないエラーはアプリケーション中で復旧が困難で重度なものであるから。
Try以外でも、扱うことができる全ての例外をまとめて処理する場合などに以下のようなコードが出てくることがある。

OptionとEitherとTryの使い分け

  • Optionは、Javaでnullを使うような場面で使うのが良い。
  • Eitherは、Optionを使うのでは情報が不足しており、エラー状態が代数的データ型として定められるものに使うのが良い。Javaでチェック例外を使っていたような復帰可能なエラーだけに使うという考え方でも良い。
  • Tryは、Javaの例外をどうしても値として扱いたい場合に使うのが良い。非同期プログラミングや、実行結果を保存し中身を参照する場合などに使うことも考えられる。

Implicit

Implicit Conversion(暗黙の型変換)

implicit conversionは暗黙の型変換機能をユーザーが定義できるようにする機能。
implicit conversionの定義例:
implicit conversionは大きく分けて二通りの使われ方をする
  1. 新しく定義したユーザー定義の型などを既存の型に当てはめたい場合
  1. pimp my libraryパターンと呼ばれる、既存のクラスにメソッドを追加し拡張する(ようにみせかける)使い方
1つ目の実行例:
といった形で、本来しか渡せないはずにif式にを渡すことができる。※ implicit conversion を定義することで、コンパイラで本来はif文の条件式には型の式しか渡せないようチェックしているものを通り抜けてしまうため、本当にその変換が必要であるかよく考える必要がある
2つ目の実行例1:
コンパイラは、ある型に対するメソッド呼び出しを見つけたとき、そのメソッドを定義した型がimplicit conversionの返り値の型にないか検索し、型が合ったらimplicit conversionの呼び出しを挿入する。2つ目の実行例1 では、の末尾にという文字列を追加して返すimplicit conversionを定義している。
2つ目の実行例1は、Scala 2.10以降では、以下のように書き直すことが可能
2つ目の実行例2:

Implicit Parameter(暗黙のパラメータ)

implicit Parameterは大きく分けて二通りの使われ方をする
  1. メソッドで共通に引き渡されるオブジェクト(ソケットやDBのコネクションなど)を明示的に引き渡すのを省略するために使う
  1. 型クラスを定義・使用することを可能にするために使う
1つ目の使い方の具体例)以下のようなDBとのコネクションを表す型があるとする。DBと接続するメソッドは、全てこの型を引き渡さなければならない。
この3つのメソッドは共通して型を引数に取るにも関わらず、呼び出す度に明示的にオブジェクトを渡さなければならず面倒である。上のメソッド定義をimplicit parameterを使い以下のように書き換える。
Scalaのコンパイラは、このように定義されたメソッドが呼び出されると、現在のスコープからたどって直近のimplicitとマークされた値を暗黙でメソッドに引き渡す。
値をimplicitとしてマークする例:
このようなimplicit parameterの使い方は、Play 2 FrameworkやScalaの各種O/Rマッパーで頻出する。
2つめの使い方の具体例として、の全ての要素の値を加算した結果を返すメソッドを定義したいとする。この際、「何の」か全く分かっていない場合、整数のメソッドをそのまま使うことはできない。この問題を解決するために、2つの手順を踏む。まず、2つの同じ型を足す(0の場合はそれに相当する値を返す)方法を知っている型を定義する。ここでは、その型をとする。
の定義:
ここで、の型パラメータは加算されるの要素の型を表している。また、
  • :型パラメータAの0に相当する値を返す
  • :型パラメータAを持つ2つの値を加算して返す
となっている。
次に、この型を使って、の全ての要素を合計するメソッドを定義する
後は、それぞれの型に応じた加算と0の定義を持ったobjectを定義する。ここでは、について定義する。
これで、型ののどちらの要素の合計も計算できる汎用的なメソッドができた。まとめると、以下のようになる。
型ののどちらの要素の合計も計算できるメソッドの実装例:
メソッドの実行例:
これで、目的は果たすことができたが、メソッドを使えば何のの要素を合計するかは型チェックで分かっているのだから、引数にを明示的に渡すことなく、推論してほしい。前置きが長くなってしまったが、これをimplicit parameterで実現することができる。
方法は、
  • の定義の前にimplicitと付ける
  • の最後の引数リストのにimplicitを付ける
だけである。
implicit parameterを使い、を明示的に渡すことなく推論可能にした形は以下の通りである。
実行例:
この用法は、Scalaの標準ライブラリにも使われており、
のように、整数や浮動小数点数の合計値を気にすることなく計算することができる。このように、型クラスを定義・使用することを可能にするために使うことで、コード設計の幅が広がる。

おわりに

内容を振り返ってみて、Scalaの基礎文法などを一通り学ぶことができました。これからも、後から見直しやすいようにまとめていくことは続けていきたいと思います。
本記事は、Scala研修テキストよりライセンスCC BY-NC-SA 3.0に基づき、内容をまとめさせていただきました。
書籍だと以下の本が良かったです😄
💡
この記事はこちらのクロスポストです

小幡 十矛(Obata Tomu)|価値を共創するエンジニア
東京を拠点に、アプリ開発・新規事業立ち上げ・ブランドづくりに取り組んでいます。
2021年にサイバーエージェントへ新卒入社後、ABEMA Live や AmebaブログのiOSアプリ開発を担当
現在はフリーランスとして、複数の新規プロダクトやリアル店舗の立ち上げに挑戦中です。
🎯 Mission|人の挑戦を加速する仕組みをつくる ── アイデアを形にし、前に進める“土台”や“場”を共創する
📌 「リアルな場 × デジタル」の可能性を探求し、新しい挑戦が生まれる土壌を育てる
🔹 エンジニアリング × ビジネスの視点から、アイデアを形にし、成長を支えていく
🔹 実店舗オーナーを目指し、オーダースーツ・ドライヘッドスパ・セレクトショップの複合店舗の立ち上げにも挑戦中
👥 特に、社会人1〜5年目くらいで
「やりたい気持ちはあるけど、最初の一歩に迷っている」方へ。 「自分の可能性をもっと広げたい」 「モヤっとしたアイデアがあるけど、どう進めていいかわからない」 そんなフェーズにいる方と、一緒に考えたり、手を動かしたりできたら嬉しいです。
🌱 「挑戦したい20代」との出会いも、大切にしています。 「ちょっと話してみたいな」くらいの気持ちで、気軽に声をかけてもらえたら嬉しいです!🙌
📷 Instagram(リアルイベントや趣味の発信中心): https://www.instagram.com/tomu28creator
🌐 詳しいプロフィールはこちら https://obata-tomu.jp/