2014-09-19

ラムダ式の美学

art-of-lambda.png

[ 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);
}

// (以下同様)

まとめ

少しラムダ式が分かってもらえたでしょうか。

是非使ってみてください。

Java8ではじめる「ラムダ式」 (I・O BOOKS)

工学社
売り上げランキング: 163,529

コメントはTwitterアカウントにお願いします。

RECENT POSTS


[ PR ]

.