ScalaとRubyのコレクションで使える.mapについてちょっと調べてみた

突然ですが

先日、後輩くんがこんな実装をしました。

_nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

_divisible = _nums.select{ |n| n % 2 == 0 }

p _divisible
# [2, 4, 6, 8, 10]

_multiplied = _divisible.map {|n| n * 100 }

p _multiplied
# [200, 400, 600, 800, 1000]

実際はプロダクト上に実装したコードと少し違いますが、要件としては「コレクションの中から該当するものだけにしぼり、処理を施して新しいコレクションにする」という感じです。

さらにこれを「select」と「map」をつなげ、最終的には下記のような形となりました。

_nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

_multiplied = _nums.select{ |n| n % 2 == 0 }.map {|n| n * 100 }

p _multiplied
# [200, 400, 600, 800, 1000]

実は私、2年ほど前までScalaを書いていたのですが、上記のような書き方で結構実装した覚えがあります。

Scalaについて

Scala(スカラ(SKAH-lah[1])はオブジェクト指向言語関数型言語の特徴を統合したマルチパラダイムプログラミング言語である。 引用:Wikipedia - Scala

独学で2年、業務で2年ほどScalaを書いていた私ですが、詳しいScalaについてのお話はなるべく避けていきます・・・笑

ですが、例えば上記のコードをScalaで実装すると、こんな感じになるかと思います。

val nums = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

val multiplied = nums.filter { n => n % 2 == 0 }.map { n => n * 100 }

multiplied.map(println)

どうでしょう?

ほぼ同じ様に書けるんですよね。

Scalaの場合はパターンマッチというのがmapの関数内部に適用出来ますので、例えばこんな感じに書けます。

val nums = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

val multiplied = nums.map {
  case n if n % 2 == 0 => Some(n * 100)
  case _ => None
}.flatten

multiplied.map(println)

SomeとNoneはOption型というものでして、値がある場合Some、ない場合はNoneという子のオブジェクトで表現することにより、nullを徹底的に排除したプログラミングが出来るといったものです。※表現間違っていたらスミマセン

最後にOption型のコレクションに対して.flattenすると、Noneの要素が排除され、Someの要素が残り期待する結果が取得できます。

まあ、実際このような書き方をするのであれば、最後に書いたcollectメソッドを使います。

まとめ

Rubyを書きはじめた当初はあまり興味のなかった私ですが、こういった様にScalaと同じ様にプログラミング出来るのが意外でして、最近ではRubyも楽しいと感じるようになりました。

RubyのArray.mapの実装

さて、ここからは完全に余談です。

気になったのでRubyのArray.mapの実装を見てみました。

多分下記だと思っているのですが・・・スミマセン、github上でざっと見た感じですので憶測も含んでます!

https://github.com/ruby/ruby/blob/trunk/array.c#L2730-L2763

簡単に説明させていただくと、対象のArray内要素の件数分forでループし、同じく指定したブロック処理(rb_yield_force_blockargで、.map{ |n| ここの箇所 })を実行して新しいArray(コレクション)に詰めて返却するというもののようです。

一報、Scalaの.mapは、例えばcollection/immutable/List.scalaで見ることが出来ます。

https://github.com/scala/scala/blob/2.13.x/src/library/scala/collection/immutable/List.scala#L222-L236

簡単に解説すると、要素の先頭から順に受け取った関数(.map(A => B))に渡して処理を実行する感じですかね。

ちょっと詳しく説明しだすと終わらない気がするのでこの辺で・・・笑

collectと言えば・・・

さらに余談ですが、RubyのArray.mapはcollectの別名なんですね。

https://github.com/ruby/ruby/blob/trunk/array.c#L6449

実はScalaにもcollectというメソッドがありまして、先述の例題を実装しなおすと

val nums = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

val multiplied = nums.collect {
  case n if n % 2 == 0 => n * 100
}

multiplied.map(println)

と書くことが出来ます。

どうでしょう?記述量がぐっと減ったように感じませんか?