[AD] Scalaアプリケーションの開発・保守は合同会社ミルクソフトにお任せください
List
やOption
の中身を順次処理したいけど、どうするんだっけ?for
を使えばいいんだっけ?という方も多いと思います。
本記事では、Scalaのmap
メソッドの使い方や、使う場面を解説します。
サンプルコードも掲載しましたので、ぜひ手元で動かしてみてください。
では、早速見ていきましょう。
map
メソッドとは何か?
まずはmap
メソッドの基本について説明します。
map
メソッドは、要素の一つ一つに対して処理を行い、その結果に対応したそれぞれの値を格納して返すメソッドです。
map
の語源は "mapping"。
日本語で表すと「対応付け」です。
そのまんまですね。
mapメソッドはList
, Set
, Map
, Array
, Option
, Either
, Try
, Future
などのクラスに定義されています。
これらのクラスを使用する機会はとても多いですから、汎用性のきわめて高い基本のメソッドと言えます。
ただその一方で、ウッカリするとエラーになったり、 別のメソッドを使うことでよりキレイに書き換えることができたりと、 使い方一つで如実に腕前が現れる奥深いメソッドでもあります。
map
メソッドはどのように使えばいい?
ここではmap
メソッドの具体的な使い方を解説します。
List
の場合を見てみましょう。
まずは、サンプルで使うメソッドとリストを準備します。
与えられた数字を自乗して返すメソッドと、いくつかの数字を持つリストを宣言します。
そして、リストの値をそれぞれ自乗する処理を行ったのち、処理したリストを標準出力に出力することにしましょう。
def square(n: Int): Int = n * n
val numbers: List[Int] = (1 to 5).toList
val squaredNumbers = numbers.map { n => square(n) } println(squaredNumbers)
map
メソッドの内部では、各要素をスコープ内変数のn
として束縛しています。
この変数に対する処理を記述することで、それぞれの要素に対して処理を実行することができます。
出力結果は以下のようになります。
map
メソッドの機能である、各要素に処理を適用することが正しくできていることに注目してください。
List(1, 4, 9, 16, 25)
scala.collection.immutable.List#map
省略記法について詳しく教えて!
map
メソッドは、省略記法を使って短く簡潔に記述することができます。
省略前と省略後を比較してみましょう。
例A:
val squaredNumbers = numbers.map { square(_) } println(squaredNumbers)
例B:
val squaredNumbers = numbers.map { square } println(squaredNumbers)
例C:
val squaredNumbers = numbers.map(square) println(squaredNumbers)
このようにScalaにおいては複数の書き方ができますが、意味は一つです。
- 変数
n
として束縛していたのを、プレースホルダとしてアンダースコア(_
)を使用することで、文字を省略することができます。 - さらに、スコープ内の変数もメソッドが受け取る引数もそれぞれひとつです。
変数をメソッドに渡すことは(_)
を書くまでもなく自明なわけですから、省略することができます。 - 最後に、複数行の式を表す波括弧
{ }
は、一行だけの式を表す括弧( )
に置換することができます。
また、次の例では、値を格納するだけのクラスValue
を宣言し、リスト内の値をそれぞれValue
に格納します。
Value
クラスを文字列に変換して、それぞれ標準出力します。
case class Value(value: Int)
val numbers: List[Int] = (1 to 5).toList
numbers.map { n => Value(n) }.map { v => v.toString }.foreach { s => println(s) }
省略記法を駆使すると以下のように書くことができます。
アンダースコアを使ったり、アンダースコア自体を省略することによって、map
メソッドやforeach
メソッドをそれぞれ簡潔に記述することができました。
numbers.map(Value(_)) .map(_.toString) .foreach(println)
出力結果は両方とも以下のようになります。
Value(1) Value(2) Value(3) Value(4) Value(5)
map
とforeach
との違い
さて、上述の例ではforeach
メソッドなるものが出てきました。
foreach
は、map
のようにコレクションの各要素を順次処理することができますが、
行う処理自体は副作用を発生させるだけで何も値を返さない処理である場合に使用します。
実際に、上述の例ではforeach
メソッドの中でprintln
を実行していますが、
println
自体は標準出力に値を出力するのみで、返り値の型はUnit
です。何らかの意味のある値を返すわけではありません。
値を返す場合にはmap
メソッドを使いましょう。
scala.collection.immutable.List#foreach
println(x:Any): Unit
応用的な使い方
パターンマッチと組み合わせて条件分岐に柔軟に対応しよう!
例として、Option
が入ったリストを処理してみましょう。
各要素はOption
なので、それぞれSome
である場合とNone
である場合が考えられますね。
ここでは、Some
の場合とNone
の場合で異なる処理を行ってみましょう。
具体的には、Some
の場合には数字であることを示す文字列とその数字、None
の場合には空っぽであることを示す文字列を出力してみます。
波括弧{ }
の中でcase
キーワードを使うことによって条件分岐することができます。
val ints: List[Option[Int]] = List(Some(1), Some(2), None, Some(3)) ints.map { case Some(n) => s"Number: $n" case None => "Empty!" }.foreach(println)
この場合は、出力は以下のようになります。
対応する文字列がそれぞれ出力されているのがわかります。
Number: 1 Number: 2 Empty! Number: 3
連続するmap
メソッドはfor
式で処理しよう!
Valueクラスの例で説明します。
def square(n: Int): Int = n * n
case class Value(value: Int)
val numbers: List[Int] = (1 to 5).toList
numbers.map { n => Value(n) }.map { v => v.toString }.foreach { s => println(s) }
これをfor
式を使って以下のように書き直すことができます。
for
式の中でも代入記号を使用できることに注目してください。
for { n <- numbers value = Value(n) s = value.toString } println(s)
出力は変わらず、以下のようになります。
Value(1) Value(2) Value(3) Value(4) Value(5)
for
式については、こちらの記事で詳しく解説しています。
よくある失敗パターンは?
map
メソッドを使う上でよくある失敗として、2つ挙げることができます。
map
メソッド内でcase
句を使ってパターンマッチをする場合Set
クラスやMap
クラスのmap
メソッドを使う場合
これらの場合には思わぬ挙動をする可能性があるので注意してください。
以下で詳しく見ていきましょう。
パターンマッチに関する失敗:すべての場合を網羅せよ!
case
を使って上手に分岐しようとした場合にscala.MatchError
が投げられる場合があります。
条件を網羅するか、代わりにcollect
メソッドを使いましょう。
Option
の例
先程のOption
の例では、Some
かNone
のいずれかの場合のみしか書かないと、書かなかったほうの値が渡された場合にscala.MatchError
が投げられます。
val ints: List[Option[Int]] = List(Some(1), Some(2), None, Some(3)) ints.map { i => i match case Some(n) => s"Number: $n" }.foreach(println)
この場合は以下のような警告が出るのでわかります。
というのも、Option
をパターンマッチした場合はコンパイラが網羅性を検査をしてくれるからです。
これによってうっかりミスを防ぐことができます。ありがたいですね。
[warn] -- [E029] Pattern Match Exhaustivity Warning: Sample.scala [warn] | case Some(n) => s"Number: $n" [warn] | ^ [warn] | match may not be exhaustive. [warn] | [warn] | It would fail on pattern case: None
case 内で if を使う場合
もう一つの例として、1から3までの数字のうち、奇数のみを選択的に出力したい場合を見てみましょう。
(1 to 5).map { case n if n % 2 == 1 => s"odd: $n" }.foreach(println)
奇数の場合のみ出力しようとしましたが、このコードでは偶数に対応する処理が渡されていないので、scala.MatchError
が発生してしまいます。
この場合には警告は何ら発せられません。
if
を使った場合はScalaコンパイラが網羅性を検査できないからです。
奇数の場合だけでなく、奇数の場合と偶数の場合の両方の処理を渡しておけば、処理してくれます。
(1 to 5).map { case n if n % 2 == 1 => s"odd: $n" case n if n % 2 == 0 => s"even: $n" }.foreach(println)
以下のように、奇数の場合と偶数の場合の両方で結果が出力されます。
odd: 1 even: 2 odd: 3 even: 4 odd: 5
また、奇数だけ残したい、偶数は捨てたい、という場合には、より便利なcollect
というメソッドがあります。
collect
メソッドを使うと、特定の場合のみ抽出して処理を適用することができます。
collect
メソッドはfilter
メソッドとmap
メソッドを組み合わせたメソッドです。
(1 to 5).collect { case n if n % 2 == 1 => s"odd: $n" }.foreach(println)
以下のように、奇数の場合のみ出力されます。
odd: 1 odd: 3 odd: 5
scala.collection.immutable.List#filter
scala.collection.immutable.List#collect
Set
やMap
のmap
メソッドに関する失敗:処理結果が潰されてもいいのかをまず確認!
Set
クラスやMap
クラスのmap
メソッドを使う場合は、思わぬ挙動をする可能性があります。
それは、「計算結果が同じ要素は一つに潰されてしまう」という点です。
処理結果が潰されてしまう例
例として、Set
に含まれた数字のそれぞれについて、奇数なのか偶数なのかを判定して出力するプログラムを考えてみましょう。
ここでは、8つの数字が与えられています。
同じ数字に対して処理をしても無駄なので、Setを使って重複なく判定しようとしています。
このプログラムを実行するとどうなるか、考えてみましょう。
import scala.collection.SortedSet val intSet: SortedSet[Int] = // 1から5までの5つの数字が含まれる SortedSet(1, 1, 2, 2, 3, 4, 4, 5)
intSet.map { case n if n % 2 == 1 => "Odd!" case _ => "Even!" }.foreach(println)
出力結果はこちらになります。
意図した通りの挙動になるでしょうか?
Even! Odd!
おかしいですね。
それぞれについて奇数なのか偶数なのかを判定して出力していれば、合計5件出力されるはずです。
しかし、「奇数」「偶数」それぞれ1つずつしか出力されていませんね(ソートされているので"O"よりも"E"が先にきます)。
さきほどの例では、intSet
に含まれる5件の要素のうち、Odd!"となる要素が3件、"Even!"となる要素が2件ありますが、
Set
が重複を許さないことで処理結果がそれぞれ一つずつに潰され、2件しか出力されなかったというわけです。
要素の重複を許さないというSet
の特徴は、map
メソッドを使用した後にも引き続き機能するということです。
特に、「処理自体は元々の要素の数だけ行われる」「処理の回数と出力される結果の件数は異なることがある」という挙動はよく覚えておきましょう。
scala.collection.Set
scala.collection.Map
処理結果を潰さないように扱う例
では、各要素の5件の結果をすべて潰さずに出力したい場合はどうしたらよいでしょうか。
そのような場合は、List
(またはSeq
)を使えば大丈夫です。
intSet.toList.map { // toSeq でもOK case n if n % 2 == 1 => "Odd!" case _ => "Even!" }.foreach(println)
期待通りに5件出力されたことを確認してください。
Odd! Even! Odd! Even! Odd!
あるいは、今回の例に関しては、そもそもSet
ではなくList
のまま処理していれば防ぐことができました。
リストの重複を1回だけ取り除きたい場合にはdistinct
メソッドを使うことができます。
恒久的に重複を防ぎたい場合にのみSet
を使うようにしましょう。
val intList: Seq[Int] = Seq(1, 1, 2, 3, 4, 4, 5).distinct intList.map { case n if n % 2 == 1 => "Odd!" case _ => "Even!" }.foreach(println)
こちらも期待通りに5件出力されることを確認してください。
Odd! Even! Odd! Even! Odd!
scala.collection.immutable.List#distinct
以上のように、Map
やSet
のままmap
メソッドを使用すると予期せぬ挙動をするおそれがあります。
Set
やMap
のmap
メソッドを使用する際には、計算結果を潰していいのかどうか、あらかじめよく確認するようにしてください。
補足
mapメソッドとMapクラスの共通点、相違点は?
map
メソッドのほかにMap
クラスも存在することは上で触れたとおりです。
map
メソッドとMap
クラスの共通点は、それぞれ "mapping" (対応付け)という同じ単語を語源とすることです。
相違点は、map
メソッドが、何らかの処理を与えると、それぞれの要素に対してその処理を適用して、それぞれに対応する処理結果を返す処理であるのに対して、
Map
クラスは、処理にかかわらず要素("key"と呼びます)それぞれに対応する処理結果("value"と呼びます)を格納するデータ構造とみることができます。
まとめ
解説は以上です。
map
メソッドは中身の要素を順次処理できる一方、使い方を誤ると予期せぬ結果をもたらすことがわかりました。
また、特定の場合にはmap
メソッドよりももっと便利なメソッドがあることもわかりました。
なにか疑問や不明な点があれば、右下のチャットボタンから感想をお気軽に送信してくださいね。