9 日目! もうすぐ折り返し地点ですね。
今日の記事は今年ラクスから分社子会社化した『ラクスパートナーズ』のエンジニアが担当いたします。
内容は 8 月から学び始めた Scala についてです。
はじめに
Java 出身のエンジニアで Scala に初めて触れた際、その言語仕様の多さに驚いた方は少なくないのではないでしょうか? 少なくとも私は結構面喰いました。
しかし、私のようにお客様先に常駐するエンジニアは現場に出てから今まで使ったことがない言語・技術を求められることが日常茶飯事です。もちろん事前に使用技術はお聞きしてますけれど。
幸いにして弊社とお取引いただいているお客様はキャッチアップ時間も工数として積んでくださる会社様がほとんどとです。とは言えアウトプットが無い期間はお客様からすれば単純にコストでしかないのでできるだけ早くアウトプットできるようになりたいと言うのもまた事実。
そこでこの記事の方向性としては「Scala を使い始めたらまずどこを重点的に抑えれば良いか?」を私個人の独断と偏見で伝えできればと思います。この内容さえおさえておけば達人とは到底言えないまでも一応 Scala でお仕事できるかと思います。そうなればあとはより良い書き方を模索しながら開発してれば上達できるはずです。
正直言語の機能をまだまだ全ては使いこなせていないので Scala を網羅的に学習したいという場合は他の良い記事や書籍をあたった方が良いと思います。
想定している読者
ごめんなさいプログラミングの初学者には向かないと思います……。
- Scala を使ってみたい(使うことになった)けれど、言語仕様が多すぎて何から覚えて良いかわからない方
- Scala の初心者ではあるが Java のプログラミングをある程度理解している方(他の言語でも大丈夫かもしれません)
辺りの方を想定しています。
Scala の学習方法について
私は Scala を学習する際、
- 通称「コップ本」を一通り読む(全部を理解はしていません。全体的に舐める感覚です)
- List に対する既存のメソッドと同じものを『こちら』を参考に実装する。tail、reverse とか。パターンマッチとかも使って実装してみる
- 実務を通してひたすらコードを読む・書く。隙あらば Java にない機能を使っていく
という感じで進めました。
実務で作っているものはバッチ処理です。ファイルの I/O や DB の I/O があるようなバッチ処理です。
ピュアな Scala に少しライブラリを追加した程度のものでマルチスレッドとかはまだやったことないです。
Play フレームワークで web アプリケーションとかも作っていので Scala のその辺のことはわかりません。
使いこなせていない機能
以下の内容についてはこの記事では触れません。
- カリー化
- 部分適用
- 暗黙の~(型変換・パラメータ・クラス)
- Future、Promise
- Either
- モナド
- Scala Test
- GUI プログラミング
- XML 操作
- 他多数
私自身「あるのは知っているが使いどころがわからない、使い方を覚えていない」というレベルなので……。
仕事のために覚えたこと・便利だと思ったこと
上のものほどよく使う or 早く使えるようになったことと思っていただいて差支えありません。
下にいくほど抽象的な概念のお話になっていくのでうまく伝わるか若干不安です。
stb って何ぞ
Java で言うところの Maven や Gradle などと言った依存性解決・ビルドツールです。
Maven Repository から使いたいライブラリ・フレームワークの SBT というタグの内容を build.sbt に記述するとその機能が使えるようになります。
Scala とは別にインストールしてパス通す必要があったはずです。
できるだけ immutable に書く
他の言語から入ってくると変数やコレクションに後から値を入れない書き方というのが最初なかなか理解しにくい概念になると思います。
Scala では基本的に再代入等はアンチパターンと考えられます。
(状況によりけりで必ずしもアンチパターンとは限りません)
Scala ではこれらの不変性を保証するために val による変数宣言、パターンマッチ、if 式、map・filter 等何らかの値を返す構文を使って一個の処理で一個の値を作るように処理を書きます。
そして、返ってきた値をまた別の処理に渡して~という感じで処理を記述します。個人的には Linux コマンド(シェルスクリプト)のパイプラインと近い頭の使い方をする言語だと感じました。
式と文
Scala では値を返すものを「式」、返さないものを「文」と呼び区別します。
if、for、match 等は結果として値を返すので「式」と呼ばれます。
変数の定義、クラス定義等は値を返さないので「文」と呼ばれます。
Scala では if や for が値を返すという点が他の多くの言語と異なる点です。
この値を返すという仕様のおかげで再代入しなくても済む場合が多々あります(if の条件次第で代入したい値が変わるとか)。式は評価された結果値を返すということは当然、変数に式を代入できます。
トレイトのミックスイン
トレイトとは「Java で言うインターフェースのすごい版」程度の認識です。
インターフェースと違って定数以外のフィールドを定義できます。定数でないフィールドの場合はインスタンス化する際にコンストラクタ等で渡してあげる必要があります。
インターフェースと違ってメソッドを実装することができます。
抽象クラスと違って複数をミックスインできます。同じシグネチャのメソッドがあった場合は基本的には後からミックスインされたもので上書きされます。(override 修飾子をつけないとコンパイルエラーになります)
「基本的には」ということはもちろん少し踏み込んだ別の挙動を表現する書き方もあります。それはコップ本では「変更の積み重ね」と表現されているものです。これは説明するよりもコードで見た方が早いと思います。
import scala.collection.mutable.ArrayBuffer
object QueueExample {
def main(args: Array[String]) {
val queue1 = new BasicIntQueue with Doubling with Add5
// 5 を足してから、2 倍する
queue1.put(10)
queue1.put(20)
println(queue1.get()) // -> 30
println(queue1.get()) // -> 50
val queue2= new BasicIntQueue with Add5 with Doubling
// 2 倍してから、5 を足す
queue2.put(10) // -> 25
queue2.put(20) // -> 45
println(queue2.get())
println(queue2.get())
// 以下は Other クラスが IntQueue を継承していないのでエラーになる
// val o = new Other with Add5
// o.put(10)
// o.put(20)
//
// println(o.get())
// println(o.get())
}
}
abstract class IntQueue {
def get(): Int
def put(x: Int)
}
class BasicIntQueue extends IntQueue {
private val buf = new ArrayBuffer[Int]
override def get(): Int = buf.remove(0)
override def put(x: Int): Unit = {buf += x}
}
/*
IntQueue 専用の拡張
*/
trait Doubling extends IntQueue {
abstract override def put(x: Int): Unit = {super.put(x * 2)}
}
/*
IntQueue 専用の拡張
*/
trait Add5 extends IntQueue {
abstract override def put(x: Int): Unit = {super.put(x + 5)}
}
class Other {
private val buf = new ArrayBuffer[Int]
def get(): Int = buf.remove(0)
def put(x: Int): Unit = {buf += x}
}
この例で示した通りミックスインする順番によって処理が適用される順番も変わります。後ろから順番に処理が適用されます。
ちなみにこの例の Add5 と Doubling は IntQueue を継承しているため IntQueue を継承したクラス・トレイトにしかミックスインできません。コメントアウトされている Other クラスは BasicIntQueue と全く同じ定義ですが Add5 や Doubling をミックスインするとエラーになります。
こんな感じでトレイトを積み重ねることで柔軟に処理内容を変更可能です。
Scala における抽象クラスとの使い分けについてですが、私もぶっちゃけよくわかっていませんw
コップ本には「使い分けに悩んだらトレイトにしとけば問題ないよ」的なことが書かれていたと思います。
Option
Java 8 以降で使えるようになった Optional と同じものと思って大丈夫なはずです。(実務で Java 8 ほとんど使ってないですが……)
処理中に null が発生し得る値(DB から特定の条件で値取ってくるとか)は Option(取得するつもりの値)
と書いておきます。
こうすることで実際の処理の結果、取得しようとした値が存在した場合は Some(取得できた値)
、存在しなかった場合は None
という値になります。
これの何が良いかと言うと Some も None も Option という抽象クラスを継承しているので値が取れようが取れまいが気にせずその後の処理を抽象的に書くことができます。値の有無はその値を使うときまで気にする必要がなくなります。
Option に対して後述のパターンマッチを書いて値が存在した場合・しなかった場合の分岐をシンプルに書くこともできて便利です。
Scala では null になりそうな場合は基本的に Option でラップするので変なタイミングで NPE が発生することがなくなります。(Option が使われているということは値が取れない場合もあるということが見ただけで伝わるので考慮が漏れるということを防げます)
ちなみに Some に対して get というメソッドを実行すると Some でラップされている値が取れますが None に対して get を実行すると例外が発生します。get って何のためにあるメソッドやねん……
パターンマッチ
Scala すごいぜシリーズその 1 。
Java で言うところの switch 文くらいかなと思っていたらそれよりもはるかに多くの表現が可能です。
これは他の方の投稿(こちらとか)できれいにまとまっているのでそちらを見ていただいた方が良いと思います。
個人的によく使うかなと思うのは↑のリンク先で挙げられている「定数」「型付き」「タプル」「固定長・任意長シーケンス」「コンストラクタ」「変数」「ワイルドカード」「Option」「regular expression」あたりのパターンマッチです。
DB から値を取って取れたら云々という処理を書くことが多いので「Option」のパターンマッチは特によく使います。
foreach、map、filter、flatMap
Scala すごいぜシリーズその 2 。
コレクションクラス(List、Map 等)はだいたいこれらのメソッドが使えます。
コレクション内のそれぞれの要素を使って処理を行います。
Java だと拡張 for 文書くような処理もほとんどこれらのメソッドでこと足りてしまいます。
(その証拠に私は Scala の for 式の書き方をはっきり覚えていませんw)
Java 8 以降であれば StreamAPI で同じような書き方ができますが Java だといちいち Stream を取得する処理やら終端操作書かないといけないのが……。
コレクション操作メソッドは本当はもっと色々ありますが最初は以下の 4 つ覚えれば良いかなと思います。使い分けは以下の通り
メソッド名 | 使う場面 |
---|---|
foreach | コレクション内の要素それぞれを使って副作用(戻り値を伴わない処理)を起こしたい場合 例)ログ出力、DB 書き込み等 |
map | コレクション内の要素それぞれに何らかの処理をして別のコレクションを作りたい場合 例)List[Person] から name というフィールドだけ抽出して List[String] にして返したい等 |
filter | コレクション内の要素それぞれに何らかの判定処理を行い、true だった要素のみのコレクションを作りたい場合 例)List[Person] の age >= 20 を満たす要素を抽出して成人の List を作る等 |
flatMap | コレクション内の要素それぞれに map と flatten を両方適用したいとき 例)Some(x) と None が混在するコレクションの None でない要素それぞれに何かしたコレクションを作る等(他多数) |
第一級関数、高階関数
Scala すごいぜシリーズその 3 。
第一級関数というのは関数を一つの値として扱える言語の性質のことです。
と言ってもなかなか分かりにくいので一応例を挙げてみます。
package jp.co.example
object Main {
def main(args: Array[String]): Unit = {
// 変数 printString の中に関数を入れる
val printString: String => Unit = {
arg =>
println(arg)
}
printString("hello world")
}
}
この例では変数 printString の中に String を引数に取って値を返さない関数を入れています。
String => Unit
の部分が printString の型です。関数を値として扱う場合の型を表す際は 引数の型 => 返り値の型
という風に書きます。
そして、arg =>
の部分で引数として受け取った文字列を arg という変数に束縛します。関数内では arg を通して引数を使うことができます。
次に高階関数です。高階関数とは「引数や戻り値の型が関数になっている関数」のことです。一つ前のお題の「foreach、map、filter、flatMap」も高階関数の一種です。
実際に使う場合の例はコップ本のものが非常にわかりやすかったです。
コップ本の例ではファイルの open と close を行う高階関数を定義し、開いたファイルに対する処理を引数として受け取るという例が挙げられていました。この例だとファイルの open、close を共通化して、開いたファイルに対して何をするかは引数として後から渡せて良いよねっていう話でした。
高階関数を使うと処理を共通化して差分の箇所だけ後からすげ替えることが容易になります。
これだけだと今ひとつメリットが伝わらないのが残念ですが実際に使ってみてその強力さを実感していただきたいです。
型パラメータ
Scala は型をメソッドのパラメータにすることができます。イメージとしては「オーバーロードのちょっとすごい版」と言った感じでしょうか?
型パラメータはメソッドを使うタイミングでその型を指定します。
例を挙げるとこんな感じ。
package jp.co.example
object Main {
def main(args: Array[String]): Unit = {
// 型パラメータとして Int を指定
val int = typeParamExample[Int](1)
println(int)
// 型パラメータとして String を指定
val string = typeParamExample[String]("string")
println(string)
}
def typeParamExample[Type](elem: Type): Type = {
println(elem.getClass)
elem
}
}
class java.lang.Integer
1
class java.lang.String
string
今回の例ではただ [Type]
と指定しましたが例えば [Type <: java.util.Date]
と書けば Date クラスを継承したパラメータのみが指定可能になります。この場合 elem は確実に Date クラスを継承していることになるのでメソッドの中で elem は Date クラスのメソッド(getTime() とか)を使うことができるようになります。この書き方は組み込みクラス以外にも自作のクラスやインターフェースでも有効なので処理の抽象化に一役かってくれます。
また、高階関数の引数や返り値の型も型パラメータとして受け取ることができるので本当にかなり抽象的なコードが書けます。(そこまで抽象化してパッと読めるかはさておき)
所感
元々 Java を書いていた身としては事前の評判通り確かに記述量が少なくて済んで楽だなというイメージです。
Java の StreamAPI みたいなことが特に意識せずできるのもグッド。(ま、実務で Java 8 以降の機能使える機会は今までありませんでしたけどね!)
あと Java 8 以降のラムダ式、ストリーム、オプショナルの理解が正直イマイチという場合は一度 Scala を学んでから戻るとスッと納得できるのではないかな、と思いました。
ただ、言語の学習コストは正直高いかなと思いました。
一般的に学習コストがやや高いと言われている Java で 2~3 年仕事をしてきて Scala は Java の改良版みたいな話を聞いていましたが最初に Scala 独特の概念を理解するのには苦労しました。
第一級関数、高階関数、case class のパターンマッチなどなど。慣れると便利なんですけどね。
プログラミング自体の初心者には全くオススメできない言語ですが Java にある程度慣れ親しんでいる方にとっては次の言語として良いのではないかなと思います。関数型言語のパラダイム含んでるし報酬高い傾向にあるし。
以上で終わりです。
これから Scala を使うことになる方にとって少しでもお力になれたらそれはとっても嬉しいなって思います。