ループ処理をする方法は?mapメソッドの使い方

Scala 3 (Dotty 0.26.0-RC1) 2.13.3 2.12.12
最終更新:2020年5月19日

[AD] scalapediaでは記事作成ボランティアを募集しています

ListOptionの中身を順次処理したいけど、どうするんだっけ?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 Standard Library: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)

mapforeachとの違い

さて、上述の例ではforeachメソッドなるものが出てきました。
foreachは、mapのようにコレクションの各要素を順次処理することができますが、 行う処理自体は副作用を発生させるだけで何も値を返さない処理である場合に使用します。

実際に、上述の例ではforeachメソッドの中でprintlnを実行していますが、 println自体は標準出力に値を出力するのみで、返り値の型はUnitです。何らかの意味のある値を返すわけではありません。

値を返す場合にはmapメソッドを使いましょう。

Scala Standard Library: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の例では、SomeNoneのいずれかの場合のみしか書かないと、書かなかったほうの値が渡された場合にscala.MatchErrorが投げられます。

val ints: List[Option[Int]] = List(Some(1), Some(2), None, Some(3)) ints.map { 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

SetMapmapメソッドに関する失敗:処理結果が潰されてもいいのかをまず確認!

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 Standard Library:scala.collection.Set
Scala Standard Library: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!

以上のように、MapSetのままmapメソッドを使用すると予期せぬ挙動をするおそれがあります。
SetMapmapメソッドを使用する際には、計算結果を潰していいのかどうか、あらかじめよく確認するようにしてください。

補足

mapメソッドとMapクラスの共通点、相違点は?

mapメソッドのほかにMapクラスも存在することは上で触れたとおりです。

mapメソッドとMapクラスの共通点は、それぞれ "mapping" (対応付け)という同じ単語を語源とすることです。

相違点は、mapメソッドが、何らかの処理を与えると、それぞれの要素に対してその処理を適用して、それぞれに対応する処理結果を返す処理であるのに対して、 Mapクラスは、処理にかかわらず要素("key"と呼びます)それぞれに対応する処理結果("value"と呼びます)を格納するデータ構造とみることができます。

まとめ

解説は以上です。

mapメソッドは中身の要素を順次処理できる一方、使い方を誤ると予期せぬ結果をもたらすことがわかりました。
また、特定の場合にはmapメソッドよりももっと便利なメソッドがあることもわかりました。

なにか疑問や不明な点があれば、右下のチャットボタンから感想をお気軽に送信してくださいね。

サイト内検索