関数型プログラミングにおけるクイックソート・アルゴリズムの実装

Implementation of the quicksort algorithm in functional programming

Revision History
Revision 1.02013年8月25日
初公開
Revision 1.12013年8月28日
非関数型言語のコードについて、部分リストへの要素追加を止めて リストを再構成するよう変更した

Abstract

この記事では、クイックソート・アルゴリズムの実装について、 様々な言語で書かれた関数型プログラミングのコードを紹介しています

Table of Contents

はじめに
アルゴリズムの定義
Prolog - 論理型言語
関数型言語
Haskell
Standard ML
非関数型言語
Ruby
JavaScript
Python
まとめ

はじめに

この記事の目的は、 非関数型プログラミング言語における 関数型プログラミングの適性度評価の元になる材料を提供する ことです。 ただし、素材は提供しますが、それら言語の(主観的な)優劣については語りません。 それでも、 関数型言語もしくは関数型プログラミングというスタイルに興味を持った人にとって、 本記事が自分にはどんな言語が直感的に理解しやすいかを判断する ヒントになるとすれば幸いです。

なお、この記事の内容は、 2ちゃんねる掲示板 のプログラム板にあるスレッド 【PHP,Python】スクリプト, バトルロワイヤル36【Perl,Ruby】 で行われた議論を整理、加筆したものです。

以降の節では、まず最初に、論理型言語であるPrologのコードで クイックソート・アルゴリズムの定義を示します。 次に、この定義に従って、関数型言語、そして関数型には分類されない言語の順に コードを見ていきます。

アルゴリズムの定義

Prolog - 論理型言語

Prologによるクイックソート・アルゴリズムを以下に示します:

% quicksort(Xs, Ys)                         ❶

quicksort([],  []).                         ❷
quicksort([Pivot|Xs], Ys) :-                ❸
    partition(Xs, Pivot, Littles, Bigs),    ❹
    quicksort(Littles, Ls),                 ❺
    quicksort(Bigs,    Bs),
    append(Ls, [Pivot|Bs], Ys).             ❻


% partition(Xs, N, Ys, Zs)

partition([], _, [], []).
partition([X|Xs], N, [X|Ys], Zs) :- X < N,  !, partition(Xs, N, Ys, Zs).
partition([X|Xs], N, Ys, [X|Zs]) :- X >= N, !, partition(Xs, N, Ys, Zs).

リストXsをソートしたものが Ysである (この行はコメントです)

リストが空であれば、ソート結果も空である

リストが空でなければ、 その先頭要素を中心軸Pivotへ、 そして残りのリストをXsへ代入する

残りのリストXsについて、 中心軸Pivot未満の値で構成される 部分リストLittles、および 中心軸Pivot以上の値で構成される 部分リストBigsへと分割する

リストLittlesをソートしたものが Lsであり、 リストBigsをソートしたものが Bsである

ソートされた部分リストLs、 中心軸Pivot、 そしてソートされた部分リストBsの順で 結合したリストをYsへ代入する

以降の節では、ここで示したアルゴリズムを どれだけ素直に(=直感的に)コードで表現できるか?が、 関数型プログラミングの適性度を測る評価基準になるでしょう。

なお、言語によっては、 汎用的なpartition操作が標準で提供されているものがあります。 そこで 表現力の公平を期すため、 以降の節では、まず最初にpartition操作無しでコードを示し、 その後でpartition操作を用いるコードを示すことにします。

関数型言語

関数型言語が関数型プログラミングを何の苦もなく実践できるのは、当たり前の話です。

Haskell

quicksort []         = []
quicksort (pivot:xs) = quicksort littles ++ pivot:(quicksort bigs)
    where
        f x (ls, bs)    = if x < pivot then (x:ls, bs) else (ls, x:bs)
        (littles, bigs) = foldr f ([], []) xs

標準関数partitionを使うと、より簡潔なコードが書けます。

quicksort []         = []
quicksort (pivot:xs) = quicksort littles ++ pivot:(quicksort bigs)
    where (littles, bigs) = partition (< pivot) xs

このコードに関数partitionの定義を追加したものが、 最終版になります。

quicksort []         = []
quicksort (pivot:xs) = quicksort littles ++ pivot:(quicksort bigs)
    where (littles, bigs) = partition (< pivot) xs


partition f = foldr g ([], [])
    where g x (ys, zs) = if f x then (x:ys, zs) else (ys, x:zs)

Standard ML

fun quicksort []          = []
|   quicksort (pivot::xs) = let
        fun f (x, (ls, bs)) = if x < pivot then (x::ls, bs) else (ls, x::bs)
        val (littles, bigs) = foldr f ([], []) xs
    in
        quicksort littles @ pivot::(quicksort bigs)
    end

標準ライブラリ関数List.partitionを使うと、 より簡潔なコードが書けます。

fun quicksort []          = []
|   quicksort (pivot::xs) = let
        val (littles, bigs) = List.partition (fn x => x < pivot) xs
    in
        quicksort littles @ pivot::(quicksort bigs)
    end

このコードに関数partitionの定義を追加したものが、 最終版になります。

fun partition f = let
        fun g (x, (ys, zs)) = if f x then (x::ys, zs) else (ys, x::zs)
    in
        foldr g ([], [])
    end


fun quicksort []          = []
|   quicksort (pivot::xs) = let
        val (littles, bigs) = partition (fn x => x < pivot) xs
    in
        quicksort littles @ pivot::(quicksort bigs)
    end

非関数型言語

前置きが長くなりましたが、ここからが本記事の本題になります。

この節で取り上げる Ruby, JavaScript, Python は どれも関数型言語には分類されていませんが、 関数型の特性を取り入れていると言われています。 そんな関数型の特性の一つが式指向であり、 これは 分岐や反復といった制御構造に関して if/case/while/for文を使わず分岐式と関数再帰だけで表現する ことを意味します。 したがって、以降の非関数型言語による関数型プログラミングでも 文指向の代わりに式指向で、 言い換えると if/case/while/for文を使用しないコード が求められます。

それでは、これら言語の関数型プログラミング・スタイルを 見ていくことにしましょう。

Ruby

def quicksort(xs)
    if xs.length <= 1
        xs
    else
        pivot         = xs[0]
        littles, bigs = xs[1..-1].inject([[], []]) { |(ls, bs), x|
            if x < pivot then [ls + [x], bs] else [ls, bs + [x]] end
        }

        quicksort(littles) + [pivot] + quicksort(bigs)
    end
end

標準メソッドEnumerable#partitionを使うと、 より簡潔なコードが書けます。

def quicksort(xs)
    if xs.length <= 1
        xs
    else
        pivot         = xs[0]
        littles, bigs = xs[1..-1].partition { |x| x < pivot }

        quicksort(littles) + [pivot] + quicksort(bigs)
    end
end

このコードにメソッドpartitionの定義を追加したものが、 最終版になります。

def partition(xs)
    xs.inject([[], []]) { |(ys, zs), x|
        if yield(x) then [ys + [x], zs] else [ys, zs + [x]] end
    }
end


def quicksort(xs)
    if xs.length <= 1
        xs
    else
        pivot         = xs[0]
        littles, bigs = partition(xs[1..-1]) { |x| x < pivot }

        quicksort(littles) + [pivot] + quicksort(bigs)
    end
end

JavaScript

function quicksort(xs) {
    function f(xs) {
        var pivot = xs[0];
        function g(pair, x) {
            var ls = pair[0], bs = pair[1]

            return x < pivot ? [ls.concat([x]), bs] : [ls, bs.concat([x])];
        }
        var pair = xs.slice(1).reduce(g, [[], []]);
        var littles = pair[0], bigs = pair[1];

        return [].concat(
            quicksort(littles), [pivot], quicksort(bigs)
        );
    }

    return xs.length <= 1 ? xs : f(xs);
}

JavaScriptはpartition操作を標準で提供しませんが、 その操作を汎用的な関数として抽象化することで、 コードの意図はより明確になるでしょう。

function partition(xs, f) {
    function g(pair, x) {
        var ys = pair[0], zs = pair[1]

        return f(x) ? [ys.concat([x]), zs] : [ys, zs.concat([x])];
    }

    return xs.reduce(g, [[], []]);
}


function quicksort(xs) {
    function f(xs) {
        var pivot   = xs[0];
        var pair    = partition(xs.slice(1), function(x) { return x < pivot; });
        var littles = pair[0], bigs = pair[1];

        return [].concat(quicksort(littles), [pivot], quicksort(bigs));
    }

    return xs.length <= 1 ? xs : f(xs);
}

このコードを最終版とします。 なお、好みしだいで、ローカル関数宣言の代わりにラムダ式でも書けます。

function partition(xs, f) {
    return xs.reduce(
        function(pair, x) {
            var ys = pair[0], zs = pair[1]

            return f(x) ? [ys.concat([x]), zs] : [ys, zs.concat([x])];
        },
        [[], []]
    );
}


function quicksort(xs) {
    return xs.length <= 1 ? xs : (
        function() {
            var pivot   = xs[0];
            var pair    = partition(xs.slice(1), function(x) { return x < pivot; });
            var littles = pair[0], bigs = pair[1];

            return [].concat(quicksort(littles), [pivot], quicksort(bigs));
        }
    )();
}

Python

def quicksort(xs):
    def f(xs):
        pivot = xs[0]
        def g(pair, x):
            (ls, bs) = pair

            return (ls + [x], bs) if x < pivot else (ls, bs + [x])
        (littles, bigs) = reduce(g, xs[1:], ([], []))

        return quicksort(littles) + [pivot] + quicksort(bigs)

    return xs if len(xs) <= 1 else f(xs)

Pythonはpartition操作を標準で提供しませんが、 その操作を汎用的な関数として抽象化することで、 コードの意図はより明確になるでしょう。

def partition(xs, f):
    def g(pair, x):
        (ys, zs) = pair

        return (ys + [x], zs) if f(x) else (ys, zs + [x])

    return reduce(g, xs, ([], []))


def quicksort(xs):
    def f(xs):
        pivot           = xs[0]
        (littles, bigs) = partition(xs[1:], lambda x: x < pivot)

        return quicksort(littles) + [pivot] + quicksort(bigs)

    return xs if len(xs) <= 1 else f(xs)

このコードを最終版とします。 なお、好みしだいで、ローカル関数宣言の代わりにラムダ式でも書けます。

def partition(xs, f):
    return reduce(
        lambda pair, x:
            (
                lambda (ys, zs) = pair:
                    (ys + [x], zs) if f(x) else (ys, zs + [x])
            )(),
        xs, ([], [])
    )


def quicksort(xs):
    return xs if len(xs) <= 1 else (
        lambda pivot = xs[0]:
            (
                lambda (littles, bigs) = partition(xs[1:], lambda x: x < pivot):
                    quicksort(littles) + [pivot] + quicksort(bigs)
            )()
    )()

まとめ

本記事を終えるに当たって、 それぞれの言語の最終版コードをまとめて示します。

Haskell
quicksort []         = []
quicksort (pivot:xs) = quicksort littles ++ pivot:(quicksort bigs)
    where (littles, bigs) = partition (< pivot) xs


partition f = foldr g ([], [])
    where g x (ys, zs) = if f x then (x:ys, zs) else (ys, x:zs)
Standard ML
fun partition f = let
        fun g (x, (ys, zs)) = if f x then (x::ys, zs) else (ys, x::zs)
    in
        foldr g ([], [])
    end


fun quicksort []          = []
|   quicksort (pivot::xs) = let
        val (littles, bigs) = partition (fn x => x < pivot) xs
    in
        quicksort littles @ pivot::(quicksort bigs)
    end
Ruby
def partition(xs)
    xs.inject([[], []]) { |(ys, zs), x|
        if yield(x) then [ys + [x], zs] else [ys, zs + [x]] end
    }
end


def quicksort(xs)
    if xs.length <= 1
        xs
    else
        pivot         = xs[0]
        littles, bigs = partition(xs[1..-1]) { |x| x < pivot }

        quicksort(littles) + [pivot] + quicksort(bigs)
    end
end
JavaScript
function partition(xs, f) {
    function g(pair, x) {
        var ys = pair[0], zs = pair[1]

        return f(x) ? [ys.concat([x]), zs] : [ys, zs.concat([x])];
    }

    return xs.reduce(g, [[], []]);
}


function quicksort(xs) {
    function f(xs) {
        var pivot   = xs[0];
        var pair    = partition(xs.slice(1), function(x) { return x < pivot; });
        var littles = pair[0], bigs = pair[1];

        return [].concat(quicksort(littles), [pivot], quicksort(bigs));
    }

    return xs.length <= 1 ? xs : f(xs);
}
Python
def partition(xs, f):
    def g(pair, x):
        (ys, zs) = pair

        return (ys + [x], zs) if f(x) else (ys, zs + [x])

    return reduce(g, xs, ([], []))


def quicksort(xs):
    def f(xs):
        pivot           = xs[0]
        (littles, bigs) = partition(xs[1:], lambda x: x < pivot)

        return quicksort(littles) + [pivot] + quicksort(bigs)

    return xs if len(xs) <= 1 else f(xs)

クイックソートという題材に限定されますが、 非関数型プログラミング言語である Ruby/JavaScript/Pythonともに、 それら言語の特徴を生かしたスタイルを貫けば (式指向 を含む) 関数型プログラミングの適性度 に明らかな差異は見らないと、 筆者は考えます。 あとはプログラマ個々の(主観的な) 好み で選べば いいのではないでしょうか。

さて、あなたは

  • どの言語が直感的に理解しやすいと感じましたか?

  • プログラミング生活の良きパートナとして、どの言語を選びますか?