ラムダ式の美学
[ PR ]
次期Java標準『Java 8』で採用されるラムダ式(λ)。
そもそもラムダ式を採用している言語は既に多数あるのです。
手続き型言語:
C++11, D言語, C#(3.0〜), Objective-C 2.0, Visual Basic .NET, JavaScript, TypeScript, Haxe, Dart, Ruby(ブロック), Python, PHP(5.3〜), Perl, Java(1.8〜), Groovy, Xtend, Julia
関数型言語:
F#, Scala, Clojure, Haskell, OCaml, Lisp, Scheme, Erlang
なぜ、本来は関数型言語由来のラムダ式を採用しているのでしょうか。
理由は、ラムダ式を使った構文の美しさにあります。
基本
ラムダ式の表記法はいくつかあって、言語によって違いますが、JavaScriptで表すと、
function(x){
return x*x;
}
となります。ちなみに f(x) = x2を表しています。
有名な略記としては、CoffeeScriptの
(x) -> x*x
や、Rubyの、
do |x|
x*x
end
があります。ちなみに今回は説明のため、
sq = do |x|
x*x
end
というRuby式表記を使います。
Map (写像), Filter (部分集合), Fold(畳み込み)
さて、ラムダ式に慣れるために、有名な構文を紹介します。
有名なラムダ式の構文に、Map, Filter, Fold(Reduce)という3種類があります。
Map (写像)
[1,2,3,4].map do |n|
n * 2
end
# [2,4,6,8]
このような構文がMap(写像)と言われます。
分かりやすいように説明すると、 [1,2,3,4] = ×2 => [2,4,6,8] というように、それぞれの要素が2倍になるように写像されています。
これを応用すると、次のようなこともできます。
[1,2,3,4].map do |n|
"n = #{n}"
end
# ["n = 1", "n = 2", "n = 3", "n = 4"]
計算だけでなく、型の変換もできるんですね。
従来のコードで書くと…
ちなみに、
[1,2,3,4].map do |n|
n * 2
end
を従来のコードで書くとこうなります。
arr = []
for n in [1,2,3,4] do
arr.push(n*2)
end
p arr # [2,4,6,8]
Java8で書いてみる
これをJava8で書くとこうなります。
import java.util.stream.Stream;
import java.util.stream.Collectors;
import java.util.*;
public class MapTest{
public static void main(String[] args){
List list = Arrays.asList(1,2,3,4).stream()
.map(n -> String.format("n = %d", n))
.collect(Collectors.toList());
System.out.println(Arrays.toString(list.toArray(new String[]{})));
}
}
// [n = 1, n = 2, n = 3, n = 4]
Filter (部分集合)
Mapと基本は同じです。
[1,2,3,4].select do |n|
n.even?
end
# [2,4]
Mapと異なるのは、Filterはその名の通り、条件に合うものを絞り込むということです。(数学で言う部分集合)
3の倍数を絞り込むにはこうします。
(1..20).select do |n|
n % 3 == 0
end
# [3, 6, 9, 12, 15, 18]
従来のコードで書くと…
ちなみに、
[1,2,3,4].select do |n|
n.even?
end
を従来のコードで書くとこうなります。
arr = []
for n in [1,2,3,4] do
if n.even?
arr.push(n)
end
end
p arr
p arr # [2,4,6,8]
Java8で書いてみる
これをJava8で書くとこうなります。
import java.util.stream.Stream;
import java.util.stream.Collectors;
import java.util.*;
public class FilterLambda{
public static void main(String[] args){
List list = Arrays.asList(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20).stream()
.filter(n -> n % 3 == 0)
.collect(Collectors.toList());
System.out.println(Arrays.toString(list.toArray(new Integer[]{})));
}
}
// # [3, 6, 9, 12, 15, 18]
Fold (畳み込み)
Foldは一番分かりづらいので、似たような機能のReduceを紹介します。
Reduce
基本
基本的なものから覚えましょう。
[1,2,3,4,5].reduce(:+)
# 15
これはExcelのSUMと同じです。それぞれの数字を足して(+)いるので、 1 + 2 + 3 + 4 + 5 = 15 となります。
応用
次はもう少し難しい例を。
[16,2,2,2,2].reduce(:/)
# 1
これは、それぞれ左から割っているので、16 ÷ 2 ÷ 2 ÷ 2 ÷ 2 = 16 ÷ (2 × 2 × 2 × 2) = 16 ÷ 16 = 1 となります。
Foldで表す
最初の例の、
[1,2,3,4,5].reduce(:+)
をFoldに変換してみます。
[1,2,3,4,5].inject(0) do |a,b|
a + b
end
少し難しくなりましたね。これは、左から ((((( (初期値) + 1) + 2) + 3) + 4) + 5) という計算をしているので、Fold Left(左畳み込み)と言われます。
逆に、右から計算する (1 + (2 + (3 + (4 + (5 + (初期値) ))))) Fold Right(右畳み込み)もあります。
(ということは、ReduceはFold Leftの省略形ということになりますね。)
RubyにはFold Rightとはありませんが、次のようにすると再現できます。
[1,2,3,4,5].reverse.inject(0) do |a,b|
a + b
end
Java8で書いてみる
Java8でFold Leftを書くと次のようになります。
import java.util.stream.Stream;
import java.util.stream.Collectors;
import java.util.*;
public class FoldLambda{
public static void main(String[] args){
Integer sum = Arrays.asList(1,2,3,4,5).stream()
.reduce((a,b)->a+b).get();
System.out.println(sum); // 15
}
}
高階関数(新しい構文を作る)
最後に、ラムダ式の醍醐味である高階関数を紹介します。
(注)これからJavaScriptを用いて説明します!
Loop文を作る
これから作るLoop文とはこのようなものです。
loop(4, function(n){
console.log(n);
})
// 0
// 1
// 2
// 3
では早速、関数を定義します。
function loop(n, func){
for(var i=0; i<n; i++){
func(i);
}
}
ここで、loop(n, func)
のfuncがまさに高階関数です。
このfuncに無名関数(ラムダ式)を代入すると、func(i);
の箇所で呼び出されます。
実行
早速実行します。
loop(4, function(n){
console.log(n);
})
// 0
// 1
// 2
// 3
例題と全く同じですね。
JQueryなどで何気なく使っているこの構文ですが、実は高階関数のパワーが活かされているのです。
真のDRY (Don't Repeat Yourself) を実現する
コーディングの重要な原則に、「自分の手で繰り返さない」というものがあります。
最初に、簡単な例を紹介します。
簡単な例
function hello(s){
console.log("hello, " + s);
}
function good_morning(s){
console.log("good morning, " + s);
}
この2つは一見妥当な分割のように思えます(場合によります)。
これを最適化するには次のようにします。
// 追加
function say(base, message){
console.log(base + ", " + message);
}
function hello(s){
say("hello", s); // 変更
}
function good_morning(s){
say("good morning", s); // 変更
}
これで、パターンがいくつになってもすぐ対応できます。
難しい例
では、次の例はどうでしょうか。
function add(a,b){
return "a + b = " + (a + b);
}
function mult(a,b){
return "a * b = " + (a * b);
}
function sub(a,b){
return "a - b = " + (a - b)
}
function div(a,b){
return "a / b = " + (a / b)
}
この4つの関数には共通点が多いのですが、これ以上の最適化はできなさそうです。
それでも無理に最適化するなら、どうすればいいでしょうか。
高階関数の本領発揮
こういう時こそ、まさに高階関数の本領発揮です。
以下のように上手に無名関数を使います。
function calc(name, f, a, b){
return name + " = " + f(a,b);
}
function add(a,b){
return calc("a + b", function(a,b){ a + b }, a, b);
}
function mult(a,b){
return calc("a * b", function(a,b){ a * b }, a, b);
}
// (以下同様)
ちょっと無理がありますが、基本はこういう感じです。
今回は簡単な例なのでインパクトは少ないですが、最適化するコード量が多いほど効果が大きいです。
番外編
ちなみに、先程の例に危険なコードを含めるならこうなります。
// セキュリティ上、問題があるコード
function calc(exp, a, b){
return exp + " = " + eval(exp); // ← [警告] 自由なコードを実行される危険がある
}
function add(a,b){
return calc("a + b", a, b);
}
function mult(a,b){
return calc("a * b", a, b);
}
// (以下同様)
まとめ
少しラムダ式が分かってもらえたでしょうか。
是非使ってみてください。