境界を越える: Haskell を使った関数型プログラミング

副作用のないコーディング

構造化プログラミングもオブジェクト指向プログラミングも、ビジネス・アプリケーションの構築方法に革命的な変化をもたらしました。しかし、プログラミング・モデルは他にも存在しており、一部の人達は、そうしたモデルの方がオブジェクト指向プログラミングよりもずっと生産的なのだ、と主張しています。この記事では、Haskellを使いながら、関数型プログラミングの基本について解説します。関数型プログラミングを学ぶことによって、Java™プログラミングに対する皆さんの考え方も変わってくるのではないかと思います。

Bruce Tate (bruce.tate@j2life.com), President, RapidRed

Bruce TateBruce Tateは父であり、マウンテンバイク乗りであり、カヤック乗りであり、そしてテキサス州オースチンに住んでいます。Joltを受賞した『Better, Faster, Lighter Java』を含め、ベストセラーとなった3冊の著書を執筆しています。最近、『Spring: A Developer's Notebook』を発刊しました。彼はIBMに13年間在籍した後、J2Life, LLC社を設立し、Java技術とRubyに基づく軽量開発戦略とアーキテクチャーを専門としたコンサルティングを行っています。



2006年 7月 18日

過去 50 年間にビジネスで使われてきた、COBOL や C、C++、そして Java 言語などといったプログラミング言語は、命令型言語(imperative language) です。つまり皆さんはプログラムに対して、プログラムがどのように動作すべきかを命令します。一方関数型プログラミング言語では、皆さんはプログラムに対して、何を行うべきかを命令するのです。この記事では、Haskellを紹介しながら、関数型プログラミングについて解説して行きます。(この境界を越えるシリーズで、もう1 つの関数型言語 (Erlang) を使った並列プログラミングを取り上げた記事をすでに読んだ人は、幸先のよい一歩を踏み出していることになります。)

私が『Beyond Java (参考文献を参照)』を執筆するための調査をしていた時、Java を超える言語に関してインタビューした専門家のうち3 人が、Haskell を探るべきだと私に助言しました。その当時私は、大部分のプログラマーにとって関数型言語はあまりにも異質なため、市場に受け入れられるのは難しいだろう、と考えていました。今でも、その気持ちは変わりません。しかし私は、関数型言語の持つ素晴らしい生産性や強力さに次第にひかれるようになりました。まだ私はHaskell を少し理解し始めただけですが、それでもHaskell は早くも、私が Javaや Ruby 言語で問題を解決する上での方法に影響を与え始めたのです。

命令型言語とその欠点

このシリーズについて

この、境界を越えるシリーズでは、著者である Bruce Tate が、「今日の Java プログラマーは、他の手法や言語を学ぶことから多くを得ることができる」という概念を押し進めます。プログラミングの世界の様相は、あらゆる開発プロジェクトにとってJava が最善の選択肢であることが明確だった頃から変わってきています。他のフレームワークもJava フレームワークと同じ構築形態をとりつつあり、また他の言語での概念を学ぶことによって、それをJava プログラミングに生かすこともできます。皆さんが書く Python (あるいはRuby や Smalltalk、その他何であれ) コードによって、皆さんの Java コーディングに対する取り組みも変わる可能性があるのです。

このシリーズでは、Java 開発とは大幅に異なりながら、同時に Java 開発にも直接応用できるプログラミング概念や手法について紹介します。場合によると、そうした技術を利用するためには、それらをを統合する必要があるかも知れません。また場合によると、そうした概念は直接利用できるものかも知れません。他の言語やフレームワークが、開発者やフレームワーク、Javaコミュニティーでの基本的な手法にまで与えうる影響に比べると、個々のツールはそれほど重要ではありません。

命令型プログラムは、明示的な順序を持った一連のステートメントから構成され、そのアルゴリズムやプログラミング構成体は、アプリケーションの状態に大きく依存します。命令型言語の持つ、「プログラムに対して、どうすべきかを伝える」というスタイルは、私たちが日々プログラミングを行う上での考え方に非常に深く根付いているため、こうした特性を持たないプログラミング言語を想像することは困難です。何と言っても私たちの考え方は、命令型言語によって形作られているのです。しかし、関数型言語による他のツールを学ぶことによって、皆さんが問題を見る上での見方を広げることができます。例えば、次のような命令型の構成概念を考えてみてください。

  • 代入 (Assignment)。命令型言語のユーザーは、他のどんなプログラミング手法よりも、代入に依存します。関数型言語では、1つの変数に対して最大 1 回の代入しか許されません。ある値を変更する代入は、すべて破壊的代入(destructive assignment) と呼ばれ、許されません。例えば、ほとんどの関数型言語では、x= x + 1 は許されません。
  • 繰り返し (Iteration)。命令型言語のプログラムは、様々なタイプのデータ構造を、繰り返しを使って処理します。多くの場合、命令型制御構造では、繰り返しを行うために破壊的代入に依存しています。
  • 副作用 (Side effect)。命令型言語において、異なる値を返す可能性のある全てのメソッドは、同じ入力に対して副作用を持っています。またアプリケーション変数の状態に影響を与える全てのメソッドも、副作用を持っています。関数型言語には副作用がありません。

関数型言語を使ったことのない人には、破壊的代入や副作用のないアプリケーションの書き方など、想像もできないでしょう。しかしこうした基本的な特徴は、命令型言語の持つ非常に大きな問題につながるのです。

  • 正しさの問題。副作用を持つメソッドは、値を返しますが、同時にプログラムの状態を変更してしまう可能性もあります。副作用があると、プログラムが正しいことを証明するために必要な計算処理が複雑になります。しかも、複雑になるのは計算処理だけではありません。副作用があることによって、プログラムの動作試験、プログラムの動作についての理解、分析、文書化も複雑になります。
  • 順序に基づくバグ。多くの場合、最適化は、鍵となるプログラミング命令の順序を並べ替えることでパフォーマンスを改善します。あるシーケンスに依存しているプログラムの順序が変更されると、誤りが発生してしまいます。
  • 並列処理の問題。mutable (変更可能) な状態がなくなると、並列処理に関わる問題も、ほとんどなくなります。BrianGoetz は『Java Concurrency in Practice (参考文献を参照)』の第 1 章を、「mutableな 状態が問題なんや、アホ (It's the mutable state, stupid) 」という言葉で締めくくっています。

信じられないと思う人もいるかも知れませんが、演算の順序を強制したり副作用を使ったりしなくても、効率的にプログラムを書くことはできるのです。


関数型プログラミングの紹介

数学では、関数は各入力を、特定の出力にマップします。関数型プログラミングでは、数学的な関数を使ってプログラムを表現します。すべての関数型言語は、コマンドを実行するのではなく、数学的な関数を表現、評価することによって問題を解決するのです。また関数型言語は通常、次のような2 つの特徴を持っています。

  • データが不変 (immutable data) 。純然たる関数型言語は、状態を保存したり取得したりするような演算に依存せず、また破壊的代入を許しません。
  • 副作用がない (No side effect) 。関数は、同じ入力を使って呼び出されると、常に同じ値を返します。

この言い方は、大多数の関数型言語を少し単純化しすぎているかも知れませんが、ほんの少しだけです。例えばmonads として知られる関数は状態の変化を数学的に表現するために使われますが、Haskellのような関数型言語では、入出力を処理し、状態変化を管理するために monadsを頻繁に使います。

関数型プログラミングの制約を幾つか見てしまったので、皆さんは、このプログラミングのパラダイムが、暗黒の時代への逆戻りだと感じているかも知れません。しかし、私を信じてください。関数型言語は役立たずではないのです。実際、言語の専門家達は一般的に、関数型言語は大部分のオブジェクト指向言語よりも高い抽象化レベルで動作すると考えています。関数型言語は、命令型言語が普通には提供できないツールを幾つか提供しているのです。この記事では、そうしたツールの実際を見て行きます。


Haskell を使う

Haskell の実装としては、Hugs と GHC (Glasgow Haskell Compiler) という 2つが有名です (参考文献を参照) 。Hugs や GHC から派生したものを含め、他にも何十というHaskell コンパイラーやインタープリターがありますが、主なものはHugs と GHC です。Haskell が初めての人には、インストールしやすく、また理解もしやすいHugs インタープリターが妥当な選択肢です。しかし、Hugs には 2 つの重要な制限があります。つまりコンパイラーがなく、スタンドアローンの関数を使えないのです。すべての関数は、1つのファイルからロードしなければなりません。本格的なプログラマーは、通常はGHC を使います。GHC のインタープリターは少し遅いのですが、コンパイラー・モードがあり、スタンドアローンの関数が許されています。この記事ではHugs を使っています。私と一緒にコーディングしようと思う人は、Hugs を使った方がよいでしょう。Hugsと GHC には、いくつかの点で違いがあるのです。

Hugs でのコーディング

自分のOS用の Hugs (参考文献を参照) をダウンロードし、起動させます。私の信頼する Macbook Pro は将来のためにとっておくことにし、すぐに環境をインストールできるWindows マシンを使うことにします。WinHugs は Hugs の実装であり、Windowsプラットフォーム用のワン・クリック・インストーラーを備えています。

インタープリター・ウィンドウに、Hugs のプロンプトが表示されていることと思います。まずは簡単なものから始めましょう。リスト1 に示すように、いくつかの数字と数式をタイプします。Hugs が数式の結果を返すのが分かります。これが、関数型言語の中で想定される振る舞いなのです。

リスト 1. Hugs インタープリターでの計算
Haskell 98 mode: Restart with command line option -98 to enable extensions

Type :? for help
Hugs>5
5
Hugs>5+4
9

Haskell には、強力な型定義モデルがあります。Haskell は強い型定義の言語です。つまり許可された演算は、ある型の、対象とするインスタンスに対してしか行うことができません。(例えばストリングに数字を加えようとすると、Hugsは文句を言ってきます。) Haskell は静的型定義なので、一旦ある値に変数を割り当てると、その変数は常に同じ型を保持します。またHaskellは、ある種の型推論を行います。つまり、プログラム中の構文的な手がかりによって要素の型を推論します。ですから皆さんは、私がいくつかの関数を、関連する型を宣言せずに使うのに気がつくかも知れません。もし関数で型をあいまいに使ったり、サポートされていない型を使おうとしたりすると、Haskellは文句を言ってきます。また Haskell はサブタイプも持っており、完全に多相的です。これらの特徴は、この記事の範囲外ですが、関心のある方は自分で調べてみる価値はあります。

整数のような基本的な型については見たので、もう少し高度な型を幾つか見てみましょう。多くの場合、ある言語の使い方は、その言語内で使用可能なデータ構造によって定義されます。例えばC では構造体を使い、Java ではクラスを使いますが、Haskell は、そのどちらも使いません。

Haskell での最も重要なデータ構造は、タプル (tuple) とリスト (list)、そしてユーザー定義型(user-defined type) です。ここでは最初の 2 つに焦点を当てることにします。タプルは( ) で囲まれ、固定長です。タプルは様々な型の基本要素を含むことができ、さらには他のタプルやリストを含むことさえできます。対照的にリストは可変長であり、同じ種類の要素で構成されます。リストは[ ] で囲まれます。リスト 2 は、Hugs を使ったタプルとリストの実験です。

リスト 2. 単純なタプルとリストを表現する
Hugs> [1,2]
[1,2]
Hugs> [1,"a"]
ERROR - Cannot infer instance
*** Instance   : Num [Char]
*** Expression : [1,"a"]

Hugs> (1,2,3)
(1,2,3)
Hugs> (1,"a")
(1,"a")

Hugs> [(1,2),(3,4)]
[(1,2),(3,4)]
Hugs> [(1,2),(3,4),(5,6,7)]
ERROR - Type error in list
*** Expression     : [(1,2),(3,4),(5,6,7)]
*** Term           : (5,6,7)
*** Type           : (c,d,e)
*** Does not match : (a,b)

リスト 2 を見ると、タプルの中の要素は異なる型でも構わないのに対して、リストの中の型は同じ種類でなければならないことが分かります。さらに、タプルのリストの場合には、それぞれのタプルは同じ長さである必要があり、またタプルの中のn 番目の要素の型は、リストの中にある他の全てのタプルの中の、n 番目の要素の型と一致する必要があります。

皆さんが想像される通り、Haskell はリストを操作するための関数を数多く持っています。最も簡単なものは、headと tail です。head はリストの最初の要素を返し、tail はそれ以外の全てを返します。リスト3 は、単純なリスト関数のいくつかを示したものです。

リスト 3. Haskell の単純なリスト関数
Hugs> [1,2,3,4,5]
[1,2,3,4,5]
Hugs> length [1,2,3,4,5]
5
Hugs> head [1,2,3,4,5]
1
Hugs> tail [1,2,3,4,5]
[2,3,4,5]

リスト 3 を見ると、head はある 1 つの要素を返し、tail は要素のリストを返すことが分かります。この先の、関数を書くでは、こうした関数がどのようにして Haskell の多くの再帰関数の根幹を形成するかを学びます。

リストを構成する場合には、cons 演算子 (construct を意味します) と呼ばれる、:演算子を使います。リストを構成するためには、単純に要素を別のリストに渡します。cons演算子は、いくつもつなげて使うことができます。

ストリングは単純に、文字のリストに対する構文糖(syntactic sugar)であり、例えば[1,2,3] のようなリストは、1:2:3:[] に対する構文糖です。この機能によって、ストリング操作の実装がずっと容易になります。リスト4 は、cons の動作と、文字のシーケンスからストリングを構成する方法を示しています。

リスト 4. cons 演算子を使ってリストを構成する
Hugs> 6:[]
[6]
Hugs> 6:[7]
[6,7]
Hugs> 6:7:[]
[6,7]
Hugs> 'd':'o':'g':' ':'h':'a':'s':' ':'f':'l':'e':'a':'s':[]
"dog has fleas"

ストリングやリストに対して見られる、こうした種類の構文糖は、Haskell では一般的なものです。しかし、よく覚えておいてください。すべては関数なのです。

関数をデータとして扱う

Haskell では、関数をデータとして扱うことができます。この重要な機能によって、多くのHaskell 関数では、関数をパラメーターとして使うことができます。この方法を利用すると、ある関数の各要素に対して、様々な形で関数を適用することができます。リスト5 は、リストの各要素に対して関数を適用する、一連の関数を示しています。

リスト 5. 関数をパラメーターとして渡す
Hugs> :l Char
Char> map toUpper "Hello"
"HELLO"
Char> filter isUpper "To Be or Not to Be"
"TBNB"
Char> foldr (max) 0 [1,2,3,2,1]
3
Char> foldr (+) 0 [1,2,3,2,1]
9

:l というコマンドは、あるモジュールをロードします。次に、Char モジュールの中の関数を呼びます。(他のバージョンの Haskell では、例えば Char.toUpper が許されていますが、Hugsでは許されていません。そのため、モジュールをロードしてから、その中で関数を使います。)最初の命令は、リストの各要素に対して、ある関数 (この場合は、文字を大文字に変換するtoUpper) を呼びます。Haskell のストリングは単に文字のリストなので、「HELLO」というストリングが返ってきます。filter関数はブール値を返すテスト関数をリストの各要素に対して呼び、そのテスト関数がTrue を返す場合のみ、その要素を含めます。

fold 関数は、もう少し複雑です。この関数には、foldl と foldr という、2 つの形式があります。fold関数は、関数や要素、そしてリストを扱うことができます。fold 関数は次のように考えることができます。

  1. まず、リストの左側 (foldl の場合)、または右側 (foldr の場合) に、要素 (2番目のパラメーター) を置く。
  2. リスト中の全要素の間に演算子を置く。
  3. リストの一方の側から (foldl の場合は左側から、foldr の場合は右側から) もう一方の側に向かって演算子を適用する。

例えば、foldr (+) 0 [1,2,3] は、1 + (2 + (3 + 0)) 、つまり6 になります。場合によると、順序が問題になります。そのためにHaskellでは、foldr (右連想 (right associative)、右から左に構成) と foldl (左連想(left associative)、左から右に構成) の両方が用意されています。リストを操作する場合には、任意のバイナリー演算の結果をまとめ上げるために、fold関数が便利です。

関数を組み合わせる

Haskell プログラムの中では、様々な方法で関数を組み合わせることができます。関数を複合的に合成することで抽象化レベルを連続的に高められるため、関数の合成は関数型言語での生産性を高める上での鍵と言うことができます。

関数を複合的に合成することで抽象化レベルを連続的に高められるため、関数の合成は関数型言語での生産性を高める上での鍵と言うことができます。

例えば、ある文の中の大文字を数える Java アプリケーションを考えてみてください。このためにはリストに対して繰り返しを行い、大文字にぶつかる度にローカル変数を1 増加させる必要があります。一方 Haskell では、単純に length (filter (isUpper)"To Be or Not to Be") を使って、フィルターされたリストの長さを取得するだけです。Haskellプログラマーは、このようにして破壊的更新を使わずに済ませるのです。それぞれの関数は中間結果を内部的に保持するため、そうした詳細の処理は私たちの負担になりません。


関数を書く

皆さんが Hugs を使う場合には、関数群を別のソースファイルとして構成する必要があります。WinHugsは私のエディターにうまく統合できるため、これは大した負担ではありません。関数群をモジュールの中に置き、そのモジュールを:l コマンドを使ってロードします。リスト 5 で、システム・モジュール Charをロードする様子をすでに見ました。モジュール名と、モジュール名を含むファイルは、大文字で始まります。関数は小文字で始まります。Haskell のプログラム・ファイルは、.hs という拡張子で終わります。

Haskell プログラムでは、問題の解決に再帰が頻繁に使われることに注意してください。Java開発者は、パフォーマンスへの影響やスタックの深さの制限などから、再帰を避けたがる場合が多いようです。Haskellには、再帰を扱う上で 2 つの重要な利点があります。つまりHaskell は末尾再帰を最適化します。またHaskell は怠け者(lazy)なのです。

末尾再帰の最適化は、関数の最後で再帰が起きる場合に、コンパイラーまたはインタープリターが、再帰を繰り返しとして表現できるということです。末尾再帰の最適化にはスタックのオーバーヘッドがないため、再帰処理のためのコストを、単純な繰り返しのコストにまで低減することができます。私が怠け者という意味を理解するためには、次の関数をタイプして、Generate.hsというファイルにしてみてください。

generate = 1:generate

この関数は、1 の無限リストです。もっと正確に言えば、generate はcons 演算子を使って、generateの結果 (最初は空のリスト) に 1 を加えます。この関数をロードして実行しようとすると、再帰を止めるものが何もないため、再帰の無限ループに陥ります。ところが不思議なことに、generateの結果をアプリケーションの中で使うことができるのです。これをリスト 6 に示します。

リスト 6. Generate.hs での怠惰な再帰
Hugs> :l Generate
Main> head generate
1
Main> head (tail generate)
1

generate は 1 の無限リストを表現していますが、Haskell はそのリストのうち、自分が必要とする部分しか計算しないのです。リスト6 では、最初のコマンドが関数をロードし、2 番目のコマンドが head (最初の)要素を取得します。そして 3 番目のコマンドは、tail の head (2 番目の要素)を取得します。Haskell は、リストの中の、最初の 2 つの要素しか計算しません。残りは後回しにされ、必要になった場合にのみ計算されます。こうした怠け者のスタイルの処理によって、関数型言語は皆さんが想像するよりもずっと効率的になり、また他のプログラミング言語では利用できない、無限ストリーム(infinitestream)と呼ばれる強力な抽象化が行えるようになります (参考文献を参照) 。

多くのチュートリアルで取り上げられている典型的な Haskell 関数は、フィボナッチ関数や階乗など、再帰的な数式関数です。フィボナッチ数列は、1と 1 から始まり、前の 2 つの数の和を数列にしたものとして定義されます。つまりフィボナッチ数列の最初の5 つの数は、1、1、(1+1) = 2、(1+2) = 3、そして (2+3) = 5 です。この数列をHaskell で実装すると、数式的な定義と非常に似たものになります (リスト 7)。

リスト 7. fib.hs によるフィボナッチ数列
fib 1 = 1
fib 2 = 1
fib x = fib(x-1) + fib(x-2)

リスト 7 のコードをタイプして Fib.hs というファイルにし、:l fib を使ってロードし、fib(4)とタイプして実行すると、この数列の 4 番目の数字が生成されます。直接の結果を保持するための変数を何も宣言する必要がなかったことに注意してください。この例を見ると、より高いレベルの抽象化が分かると思います。もし皆さんの抱える一連の問題がHaskell に適当なものである場合には、こうしたより高いレベルの抽象化が大いに役立つはずです。そうでない場合には、より高いレベルの抽象化は、むしろ呪うべきものになります。

階乗も、フィボナッチ数列の場合と同じ考え方で処理できます。x の階乗というのは、1から x までのすべての数の積です。Haskell では、階乗を計算する関数を、リスト8 のように定義することができます。

リスト 8. 階乗を計算する
factorial 1 = 1
factorial x = x * factorial(x-1)

任意のリストをトラバースする場合も同じです。リストを処理するには、最初のノードを処理してから、残りのリスト(これ自体がリストです) を処理します。リスト 9 は、リストのすべての要素の合計を計算する再帰関数を示しています。

リスト 9. 数字のリストの合計を計算する
sum [] = 0
sum (h:t) = h + sum(t)

今まで、このパターンを見たことのない人は、慣れるまでしばらく時間がかかるかも知れません。最初のラインは、空のリストの合計が0 であると言っています。2 番目のラインには、ほとんどすべての関数型言語に共通の慣用式があります。(h:t)はタプルのリストを表し、リストの先頭 (最初の要素を含みます) が h に、リストの残りがt に含まれています。末尾再帰の最適化のおかげで、この慣用式はリストに対して驚くほど簡潔に繰り返しを行うことができます。皆さんの思考プロセスは、素晴らしくコードに一致するのです。つまり最初にあるものから順に行い、残りは後にとっておき、そして何も残りがなくなったら終わり、というわけです。

もっと複雑なデータ構造のトラバースも、同じ方法で行うことができます。ちょっと努力するだけで、Bツリー (各ノードが値を保持し、2 つの分岐しか持たないツリー) を表現することができます。Haskellでの単純な B ツリーは、下記のようなものです。

data Tree a = Null | Node (Tree a) a (Tree a)

この例では、data は抽象型を作るための手段であり、a は型です。このコードはHaskell に対して、「ツリーの中身は何もない、あるいはツリーがツリーから構成される」と言っており、次に値が続き、その次に別のツリーが続いています。ツリーの各ノードには、値と、左側の子、そして右側の子があります。(子はヌルかもしれず、あるいはヌルではないかもしれません。)

ツリーをトラバースする場合も、リストをトラバースする場合のコードとほとんど同じです。ヌル・ツリーの合計をヌルにし、典型的なノードの合計を、左側のツリーの合計と、値の合計と、右側のツリーの合計を加えたものにするのです。これをリスト10 に示します。

リスト 10. ツリーをトラバースする
sum_tree :: Tree Integer -> Integer
sum_tree Null = 0
sum_tree (Node left val right) = sum_tree left + val + sum_tree right

リスト 10 の最初のラインは、この関数が使用する型を定義しています。sum_tree:: Tree Integer -> Integer は、sum_tree という関数が Integer の Treeをパラメーターにとり、Integer を返すことを意味しています。その次の 2 つのラインは、この関数を定義しています。空のツリーの合計はゼロであり、他のツリーの合計は、左側のツリーの合計にInteger の値と右側のツリーの合計を加えたものです。

これらすべてをつなぎ合わせると、リスト 11 に示す Tree.hs になります。

リスト 11. Tree.hs
data Tree a = Null | Node (Tree a) a (Tree a)

sum_tree :: Tree Integer -> Integer
sum_tree Null = 0
sum_tree (Node left val right) = sum_tree left + val + sum_tree right


t::Tree Integer
t=Node Null 4 Null

この例では、t::Tree Integer は 、ある型にTree をバインドします。次のラインは、ある値にt をバインドします。:l Tree でリスト 11 を Hugs にロードし、sum_tree tを入力します。また、もっと複雑なツリー (例えば Node Null 6 (Node Null 7Null) など) を、右側に 1 つのノードを持つツリーに対して表現することもできます。皆さんも様々なツリーをt の中に置き、再度 sum_tree を実行することで、いろいろと試してみてください。(関数を試す場合には、その都度その関数を再ロードする必要があることを忘れないでください。)


関数型の「制限」を再検証する

Haskell は、データ構造を非常にうまく扱うことができます。ストリングは単純な文字のリストとして見ることができるため、Haskellはあらゆる形式のテキストの処理に素晴らしい力を発揮するだけではなく、ツリーや高度な数式、ネスト構造なども効率的に処理することができます。言語は通常、構文のツリーとして表現されるため、関数型言語はコンパイラーや、言語を構文解析するツールの構築に頻繁に使われます。また関数型言語はデータ構造の中に関数を含めることができるため、メタプログラミングのプラットフォームの作成に最適です。

関数型言語の基本は学んだので、副作用がなく、また状態を限定サポートするだけでも、どうやってうまくやっていくか、皆さんにも分かり始めたのではないかと思います。先ほど触れた通り、monadsで状態の変化を表現することはできます。しかし monads を使ったとしても、状態を管理することはやはり困難なので、そんなことは試すべきではありません。それよりも、中間的な状態を必要としない関数の合成を定義することで問題を解決できないかを考えるべきです。繰り返しという観点から考えるのはなく、再帰や、リストの各要素に式を適用する関数(map や filter、あるいは fold など) のどれかを使うことを考えるのです。関数型言語を使うためには、命令型のプログラミング・スタイルをとにかく忘れなければなりません。関数型のスタイルでの書き方を学ぶことによって、次のようにいくつかの点で有利になるのです。

  • 多くの場合、よりコンパクトな形式で概念を表現することができます。
  • 副作用が避けられるため、アプリケーション上の、ある種のバグの影響を制限することができます。
  • ローカル変数を削減できるため、コードをスケーラブルにすることができます。

関数型言語は、プログラミングに対する考え方に劇的な影響を与えます。長年、MITのプログラマーが Lisp を最初に使ってきた理由は、より高い抽象化レベルで作業する方法を学ぶのを手助けする上で、Lispが非常に効果的であったためです。

この記事では、関数型言語の表面にちょっと触れたに過ぎません。皆さんには、Haskellやその他の関数型言語を入手して、しばらく遊んでみるようにお勧めします。それによって、きっとプログラミングに対する皆さんの新たな考え方が形成されると思います。

参考文献

学ぶために

  • Beyond Java (Bruce Tate著、2005 年、O'Reilly刊) は、この記事の著者による本です。Javaの台頭と停滞について、また、一部のニッチ領域でJava プラットフォームに対抗しうる技術について解説しています。
  • 純然たる関数型プログラミング言語、Haskell について学んでください。
  • Hugs は、純インタープリター版の Haskell です。
  • GHC (The Glasgow Haskell Compiler) は、コンパイラーとインタープリターの両方を持つ、Haskell の実装です。これは恐らく最も有名な実装だと思います。
  • 「A Gentle Introduction to Haskell」(Paul Hudak と John Peterson、Joseph Faselの共著、2000年6月、haskell.org刊) は、もう一つの Haskell チュートリアルです。この本の題名にだまされてはいけません。非常に充実した本です。
  • 「Structure and Interpretation of Programs」(Hal Abelson と Jerry Sussman、Julie Sussman の共著、1984年、MIT Press刊) は MIT の入門コースで使われており、怠惰な解釈 (lazy interpretation)と無限ストリーム (infinite stream) に関する素晴らしい内容が含まれています。
  • Java technology には、Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。

製品や技術を入手するために

  • Haskell のサイトからHugs の実装をダウンロードしてください。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=219193
ArticleTitle=境界を越える: Haskell を使った関数型プログラミング
publish-date=07182006