Lecture

関数型プログラミング Ch1 イントロダクション

資料

はじめに

本講義では,関数型プログラミング言語Haskellの基礎,使用法,及び設計に関して扱います. 想定する履修者はPythonやJavaScriptなどの手続き型言語の使用経験はあるが,関数型言語を利用したことがない大学学部生です. 関数型言語の特徴を説明する際に手続き型言語の例としてPythonでの記述が出てきますが,Pythonの文法等に関しては既知のものとして扱います.(こちらはもとも官庁用の報告書として執筆したものを(大幅に)改変したものですので,もともとの資料ではVBAやJavaを事例として用いていました.)

また,本講義では代数学を利用したプログラミングの設計に関する方法論も扱います.集合論や代数学に関する知識は前提とせず,初歩から扱いますので,数学に関する前提知識は特に必要ありません. なお,本講義は集合論や代数学の習得を目的としているわけではないので,これらに関してはかなり簡略化した説明になります.専門的に数学を学びたい方向けの講義ではないことを理解したうえで受講してください.

こちらの資料では,授業に必要な技術的な内容に限定して掲載します. 授業概要,授業の注意点,成績等については講義中に別資料で説明します.

デザインについて

文章中で色の変わっているブロックはオレンジ色が注意(warn),青色が演習や強調(note)など独立した部分を表しています. 講義ではwarnに関しては,飛ばす場合があるので,興味のある人は自分で読み進めてください.

これは注意や発展的内容を示しています.

これは演習や強調したい箇所に利用されています.

リンクはGoogleのように下線で表示されます.クリックすることでリンクに飛ぶことが可能です. 右クリックして,新しいタブで開くことを推奨しています.

プログラムブロック

本講義ではプログラムブロックは以下のように黒い背景でシンタックスハイライトが適用されています.

import pandas as pd

print('Sample')

コピー&ペーストが可能なので, 自分のプログラムに利用してください.

章立て

本講義の章立ては以下のようになっています (クリックすることで各章に飛べます.)

第1章 イントロダクション

第2章 環境構築

第3章 Haskellを使ってみよう

第4章 スクリプトファイルとエラー対応

第5章 関数

第6章 関数(応用編)

第7章 代数的データ型と関係

第8章 型クラスと代数構造

第9章 圏と多相データ型

第10章 モナドと入出力

第11章 可変状態と効果

第12章 総合演習

補足A 計算量とデータ構造の性能

現在執筆中のため,章の構成等は変更される可能性があります.

Haskellとは

Haskellは, 1987年に生まれた静的型付けの純粋関数型言語です. 本講義では Haskell の多くの特徴のうち, 代数的データ型を使ったプログラミング, つまり対象を代数的に定義し, その定義をそのままプログラムに書くことに焦点をあてます.

なぜ対象を代数的に定義するのかについて, 本講義の立場を先に述べておきます. 日本語のような自然言語で規則を説明すると, 説明できたような気になります. しかし, その説明から答えが一つに決まるかを確かめると, 決まらないことがよくあります. 厳密な構造として書けていない規則については, 答えがなぜそうなるかを説明すること (理解) も, 規則の一部を変えて新しい答えを求めること (操作) も, どこを変えると結果がどう変わるかを見通すこと (制御) もできません. そのために数学や工学では, 対象を代数的に定義したり, 型で区別したりする手法が使われてきました. Haskell は, そうして書いた定義をほぼそのままの形で実行できるプログラムにできる言語です. 本章では, この流れを一つの例で確かめます.

関数型言語の雰囲気

HaskellはLispやOCamlなどと同じ関数型言語です. 関数型言語の特徴は関数によってプログラムを構築していく点にありますが, 近年ではこのスタイルは関数型言語の専売特許というわけではなくなりつつあり, 関数で書くことの特別さは薄れつつあります. そこで, 関数型言語の細かい機能を見る前に, 関数型言語の考え方を手続き型言語との違いという観点で見ていきましょう.

関数型言語でプログラミングをするとは, 「それが何か」を分解して書いていくことです. 関数型プログラミングが宣言的であると言われる所以はそこにあります. 手続き型言語が「何をどうするのか」という手続きを書くのに対して, 関数型言語では「欲しいものは何か」を宣言します.

こちらの (Haskell界隈では) 有名なブログでは, 関数型言語の考え方について以下のように説明しています.

Functional programmers have a peculiar way of approaching problems. They start by asking very Zen-like questions. For instance, when designing an interactive program, they would ask: What is interaction? When implementing Conway’s Game of Life, they would probably ponder about the meaning of life.

関数型プログラマーは問題への取り組み方が独特だ. 禅問答のような質問から始めるのだ.例えば,インタラクティブなプログラムを設計するとき,彼らは「インタラクションとは何か?コンウェイの「人生ゲーム」を実装するとき,彼らはおそらく人生の意味について熟考するだろう.

手続き型プログラミングと関数型プログラミングの違いは色々とありますが, ここではこの文章に倣って

という観点に注目します. 例として以下の「ウサギの問題」について考えてみましょう.

ウサギの問題

  • 1つがいのウサギは, 生まれてから2ヶ月後から毎月1つがいずつのウサギを産む

  • ウサギが死ぬことはない

  • この条件の下で, 生まれたばかりの1つがいのウサギは1年の間に何つがいのウサギになるか

ウサギの問題の答えは一つに決まるか

この問題文は短く, 一度読めば分かった気になります. ところが実際に数えようとすると, 問題文だけでは決まらない点が少なくとも 2 つあります.

読み方を組み合わせると, 答えは次のように分かれます.

最初の子を産む月 数える月 答え
2ヶ月目 12ヶ月目 233
2ヶ月目 11ヶ月目 144
3ヶ月目 12ヶ月目 60

どの読み方も日本語として不自然ではありません. 問題文を書いた人は一つの読み方を想定していたはずですが, 文章にはそれが書かれていません. 自然言語の説明は, 書いた人と読んだ人の両方に「説明できた」と感じさせたまま, 答えを決めるのに必要な情報を落とすことがあります.

以下では, 0ヶ月目に生まれたつがいが 2ヶ月目に最初の子を産み, 12ヶ月目のつがいの数を答える読み方をとります.

手続きとして書く

まずは手続き型の考え方で数を数えてみます. 手続き型言語では, 「ウサギのつがいの数」を「どのように求めるのか」という手続きをプログラムに記述します.

いろいろな方法がありますが, 何が起きて, 次に何が起きて, というふうに手続きを考えると, 例えば次のような説明になります.

  • つがいを, その月に生まれた 若いつがい と, 生まれて 1ヶ月以上たった 大人のつがい に分けて, それぞれの数を記録する
    • 0ヶ月目は, 若いつがいが 1, 大人のつがいが 0
  • 1ヶ月たつと
    • 大人のつがいが 1つがいずつ若いつがいを産む
    • 若いつがいが大人のつがいになる
  • これを12ヶ月繰り返す

これをPythonのプログラムにすると以下のようになり, 結果は233となります.

months = 12  # 数える月

# 0ヶ月目の状態
young_pairs = 1  # その月に生まれたつがいの数
adult_pairs = 0  # 生まれて1ヶ月以上たったつがいの数

for month in range(1, months + 1):
    born_pairs = adult_pairs  # 前の月の大人のつがいが1つがいずつ産む
    adult_pairs = adult_pairs + young_pairs  # 前の月の若いつがいが大人になる
    young_pairs = born_pairs

print(young_pairs + adult_pairs)  # 233

このプログラムが正しく動くかどうかは, 3 行の更新をどの順序で行うかに依存します. 例えば born_pairs を使わずに, 「若いつがいが大人になる」を先に行って young_pairs = adult_pairs と書くと, 大人になったばかりのつがいもその月に子を産むことになり, 答えは 4096 になります. 正しい順序は問題文にも, 手続きの説明にも書かれていません. 手続きの説明が問題文と一致しているかを確かめるには, プログラムを 1ヶ月ずつ手で追う必要があります.

こういった考え方が, いわゆる手続き型的な考え方とプログラミングの方法になります.

それが何かを書く

では, 関数型の考え方とはどのようなものでしょうか. 先ほど引用したように, 関数型では, それが何かを考えます. つまり, ここで問われている「つがいの数」そのものの性質を記述します.

n ヶ月目のつがいの数を F_n と書きます. n ヶ月目のつがいは, 前の月からいるつがいと, n ヶ月目に生まれたつがいからなります.

0ヶ月目は生まれたばかりの 1つがい, 1ヶ月目はまだ子を産まないので 1つがいのままです. したがって, n ヶ月目のつがいの数は次の漸化式で定まります.

F_0 = 1 \\ F_1 = 1 \\ F_n = F_{n-1} + F_{n-2} \quad (n \geq 2)

この漸化式から月ごとのつがいの数を計算すると, 以下のようになります. 12ヶ月目の値は, Pythonのプログラムの結果と一致しています.

月 つがいの数 計算
0 1
1 1
2 2 1 + 1
3 3 1 + 2
4 5 2 + 3
5 8 3 + 5
6 13 5 + 8
7 21 8 + 13
8 34 13 + 21
9 55 21 + 34
10 89 34 + 55
11 144 55 + 89
12 233 89 + 144

このように, 前の数と前の前の数の和によって次の数を作る数を フィボナッチ数 といいます. 12ヶ月後のウサギの数は何かという問題は, フィボナッチ数の第12項 F_{12} は何かという問題であり, フィボナッチ数とは何かといえば上の漸化式である, と考えることができます.

普通フィボナッチ数というと, 0から始まりますが, ここではウサギの例で考えたいので1から始まることにします.

漸化式の各項は, 問題文の規則のどれか一つに対応しています. F_{n-1} は「ウサギが死ぬことはない」に, F_{n-2} は「生まれてから2ヶ月後から産む」に対応します. 式と規則の対応が 1 つずつ確かめられるので, 規則を変えたときに式のどこを直せばよいかも決まります.

例えば, 最初の子を産むのが 3ヶ月目だと読んだ場合, n ヶ月目に子を産むのは n-3 ヶ月目にすでにいたつがいなので, F_{n-2} の項だけを取り替えます. 2ヶ月目まではまだ子を産まないので, 初めの 3 項はどれも 1 です.

G_0 = G_1 = G_2 = 1 \\ G_n = G_{n-1} + G_{n-3} \quad (n \geq 3)

この漸化式で計算した G_{12} が, 「ウサギの問題の答えは一つに決まるか」の表の 3 行目の答え 60 です. 手続きとして書いた Python のプログラムで同じ変更をするには, 「生まれて 1ヶ月のつがい」を記録する変数を 1 つ増やし, 更新の順序をもう一度確かめる必要があります.

Haskellで書く

漸化式をHaskellで書くと以下のようになり, 漸化式の書き方とかなり近い対応関係があることが分かります.

fib :: Int -> Int
fib 0 = 1
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)

※メモ化とかそういったことは, 取り敢えずここでは置いておきます. (この辺の数学的定義そのままだと, 実用には向かない問題は, 後ほど扱います.)

これを実行してみると, 確かに漸化式の表と同じ数が求められています.

ghci> :{
ghci| fib :: Int -> Int
ghci| fib 0 = 1
ghci| fib 1 = 1
ghci| fib n = fib (n-1) + fib (n-2)
ghci| :}
ghci> fib 12
233

規則を変えた漸化式 G_n も, 同じように書き写せます.

lateRabbits :: Int -> Int
lateRabbits 0 = 1
lateRabbits 1 = 1
lateRabbits 2 = 1
lateRabbits n = lateRabbits (n - 1) + lateRabbits (n - 3)

lateRabbits 12 を計算すると 60 になります.

当然フィボナッチ数の漸化式は広く知られていますし, むしろ最初から漸化式として学習することが多いでしょう. したがって, Pythonでの実装もフィボナッチ数が漸化式であるという前提で, 以下のように書くほうが一般的です.

def fib(n):
    if n == 0:
        return 1
    elif n == 1:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)

また, 最近では, PythonやJavaScriptなどの手続き型の言語にも関数型の考え方が導入され, 内包表記, 再帰, ラムダ式などの関数型のシンタックスも一般的に使われるようになっています (これらの詳細についてはこのあとやっていきます). 逆にHaskell等の関数型言語においても, 手続き型のほうが便利な場合には手続き型の記法を利用します. したがって, 現在では関数型的な考え方と手続き型の考え方は, それほど明確に分かれるものではありません.

ここでは, 両者の違いを説明するために, Pythonの事例をあえてあまり用いられない方法で書きましたが, 大げさに書けば手続き型と関数型の考え方の違いとは, このような問題へのアプローチの仕方にあります.

ウサギの問題で確かめたこと

ウサギの問題を通して, 「Haskellとは」の冒頭で述べた立場を次のように確かめました.

本講義では, ウサギの問題より大きな対象 (データの集まり, 値の間の関係, 状態を持つ計算) について同じことを行います. ウサギの問題自体も, 第12章で若いつがいと大人のつがいの規則から組み直し, 漸化式を等式の変形だけで導きます.

コラム: 統計の作成手順と代数的仕様記述

ウサギの問題は非常に単純なので, 読み方の分岐はそれほど多くありませんでした. しかし対象が複雑で巨大になるほど, 自然言語による記述と厳密な記述の差は大きくなります.

その例として, 国の統計の作成手順を見てみます. 日本の産業の間の取引を一覧にした統計に 産業連関表 があり, 総務省などが 5 年ごとに作成しています. 作成の手順は担当者向けのマニュアル「令和2年 (2020年) 産業連関表作成手順 (付1 供給表の推計方法について)」として公開されており, このマニュアルを読めば, 誰が推計しても同じ値が得られることが想定されています. その中に, 工場や店舗などの事業所の売上のうち, 本業以外の副業に当たる売上を種類ごとに分ける手順があります. 要点を抜き出すと, 次のように書かれています.

  • 事業所の調査票でサービス業の売上が把握されていれば, その売上を, 事業所が属する企業の調査票にある「生産物別売上高」の構成比で分ける.
  • 企業の調査票で「生産物別売上高」が把握されない場合には, 別に集計した構成比を用いて分けるなどの対応を行う.

この手順が扱う入力の状態を, 条件の組み合わせとして並べてみます. 条件は次の 3 つです.

  1. 事業所の調査票でサービス業の売上が把握されているか
  2. 企業の調査票で生産物別の売上が把握されているか
  3. 事業所の調査票と企業の調査票の数値が食い違っているか (例えば, 事業所の売上の合計が企業全体の売上より大きい)

3 つの条件はそれぞれ「はい」か「いいえ」なので, 入力の状態は 2 \times 2 \times 2 = 8 通りあります. 手順書に処理が書かれているのは, そのうち 2 通りだけです.

事業所の売上 企業の内訳 食い違い 処理
把握 把握 なし 企業の調査票の構成比で分ける
把握 把握 あり 書かれていない
把握 なし なし 別に集計した構成比で分ける「など」
把握 なし あり 書かれていない
なし 把握 なし 書かれていない
なし 把握 あり 書かれていない
なし なし なし 書かれていない
なし なし あり 書かれていない

食い違いは, 手順書の前の段階にある「調整」で解消しておく, と読むこともできます. ただし, その調整の方法も手順書には書かれていません. さらに「把握されている」という言葉は, 調査票の欄が空欄の場合, 0 や負の値のような異常な値が入っている場合, 調査票そのものがない場合を区別していないので, 実際の分岐はもっと多くなります. 処理が書かれていない分岐は, 推計の担当者がその場で判断して埋めることになり, 担当者によって推計値が変わりえます. この 8 通りの整理は, 筆者が経済統計学会の全国研究大会 (2024 年) で報告した分析によるものです.

厳密に書くと何が変わるかを, Haskell で見てみます. コードを短くするため, ここでは条件 1 と 2 だけを型として書き, 手順書に書かれた 2 通りの処理だけを関数にします.

-- 事業所の調査票で, 副業に当たるサービス業の売上が把握されているか
data EstablishmentSales
    = SalesReported
    | SalesMissing
    deriving (Show)

-- 企業の調査票で, 生産物別の売上が把握されているか
data CompanyBreakdown
    = BreakdownReported
    | BreakdownMissing
    deriving (Show)

-- 売上を分けるときに使う構成比
data SplitMethod
    = ByCompanyBreakdown -- 企業の調査票の構成比
    | ByIndustryAverage  -- 別に集計した構成比
    deriving (Show)

splitMethod :: EstablishmentSales -> CompanyBreakdown -> SplitMethod
splitMethod SalesReported BreakdownReported = ByCompanyBreakdown
splitMethod SalesReported BreakdownMissing  = ByIndustryAverage

このプログラムを第2章で用意する環境でコンパイルすると, 次の警告が出ます (一部を省略しています).

warning: [GHC-62161] [-Wincomplete-patterns]
    Pattern match(es) are non-exhaustive
    In an equation for ‘splitMethod’:
        Patterns of type ‘EstablishmentSales’,
                         ‘CompanyBreakdown’ not matched:
            SalesMissing _

警告の最後の行は, 事業所の売上が把握されていない場合 (SalesMissing) の処理が, 企業の内訳がどちらの場合 (_) についても書かれていないことを示しています. 自然言語の手順書では, 読む人が気づかなければ残ってしまう書き漏れを, コンパイラが機械的に見つけます. 警告を消すには, 残りの場合の処理を決めて書くしかありません. 担当者の判断が必要なことは変わりませんが, 決めた処理はプログラムの 1 行として残るので, どの場合にどう処理したかを後から誰でも確かめられます.

Haskell の設計者の一人である Simon Peyton Jones は, インタビューの中で次のように述べています1.

What’s the equivalent of UML diagrams for a functional language? … it’s the type system

(関数型言語で UML 図に当たるものは何か. それは型システムだ.) UML 図はソフトウェアの構造を図で表す記法です. 型は同じ役割を, コンパイラが検査できる形で果たします.

この例のように, 対象が取りうる状態と, 使える操作の名前と型をまず書き (シグネチャ), 操作が満たすべき性質を等式で書いて (公理), 実装はその公理を満たすものの一つとして後から選ぶ設計の方法を, 代数的仕様記述 (algebraic specification) といいます. 型で場合を尽くしておけば書き漏れた場合をコンパイラが見つけ, 公理を等式で書いておけば, 公理に反する実装をテストで見つけられます. 本講義では必要な数学を少しずつ導入し, 第12章で代数的仕様記述の手順を通して使います.

関数型だと何が嬉しいのか

ウサギの問題で見た性質は, 関数型言語の利点としてよく挙げられるものの一部です. ここでは名前と要点だけを挙げ, コードを使った説明はそれぞれを実際に扱う章で行います.

ただし, 数式の世界には計算量, エラー処理, 入出力や乱数などの副作用といった概念が存在しません. そのため, 実際の複雑なプログラムを数式と完全に一致させることはできない場合があります. これらの点をどのように扱うかは, 本講義の後半で設計の話題として改めて扱います.

Haskellの特徴

ここまでは関数型言語としての考え方と利点を見てきました. ここでは視点を変えて,Haskellを広く使われている手続き型言語の代表であるPythonと比較しながら,言語としての特徴を整理していきます.

Haskellは以下の性質を備えた言語です.

対するPythonは以下の性質を持ちます.

両者を並べると以下のようになります.

項目 Haskell Python
実行方式 コンパイラ インタプリタ
パラダイム 純粋関数型 手続き型
型付け 静的 動的
評価戦略 遅延評価 正格評価
型推論 あり(強力) なし
副作用の扱い 型で区別 自由に記述可能

パラダイムの違いは「関数型言語の雰囲気」で見ました. 実行方式の違いは, プログラム全体を実行する前に機械語へ翻訳する (コンパイラ方式) か, 実行しながら命令を 1 つずつ解釈する (インタプリタ方式) かの違いです. Haskell でも, 対話的に式を評価する ghci を使えばインタプリタのように試せます. 評価戦略の違いは, 式の値を必要になるまで計算しない (遅延評価) か, 関数に渡す前に引数の値を計算しておく (正格評価) かの違いです. 遅延評価のおかげで Haskell では無限に続くリストのような値も扱えますが, 計算がいつ行われるかが書いた順序から読み取りにくくなるため, 実行速度やメモリの使用量を見積もりにくくなることがあります. 本講義では実行速度に関わる話題には深入りせず, 計算量の考え方だけを補足Aで扱います. ここでは特に「型付け」に注目します.

静的型付けと動的型付け

プログラミング言語における 型 とは,データの種類(整数,文字列,リストなど)を区別する仕組みです. 型の検査をいつ行うかによって,言語は大きく二つに分類されます.

動的型付け (Pythonなど)

動的型付け言語では,型の検査は プログラムの実行時 に行われます. Pythonでは変数や関数の引数に型を明示する必要がなく,実行して初めて型の不整合が検出されます.

def add(x, y):
    return x + y

add(1, 2)      # => 3
add("a", "b")  # => "ab"
add(1, "b")    # 実行時に TypeError

静的型付け (Haskell, Java, Rustなど)

静的型付け言語では,型の検査は コンパイル時 に行われます. コンパイルとは, プログラミング言語で記述されたコードを機械語に変換する作業をいいます. 静的型付け言語ではプログラムを実行する前に,型の不整合があればコンパイルエラーとして検出されます.

add :: Int -> Int -> Int
add x y = x + y

-- add 1 2       -- OK (結果は 3)
-- add "a" "b"   -- コンパイルエラー: Int を期待したが String

Haskellの型システムの特徴

Haskellの型システムは,静的型付け言語の中でも特に強力です.

-- 型注釈を省略してもコンパイラが型を推論する
double x = x * 2           -- Num a => a -> a と推論される
greet name = "Hello, " ++ name  -- [Char] -> [Char] と推論される

このように,Haskellでは「動的型付け言語のように気軽に書ける」面と「静的型付け言語としての強い安全性」を両立させる設計が図られています.

最近ではPythonにも typing モジュールや型ヒント(def f(x: int) -> int:)が導入されており, mypy などの外部ツールで静的型チェックを行うことも可能になっています. ただし, 型ヒントは実行時にチェックされるわけではなく, また既存コードへの普及も限定的なため, Haskellのコンパイル時型チェックとは安全性のレベルが異なります.

Haskellのメリットとデメリット及び, 比較としてHaskellがPythonに対して持つ特徴を,ここまでの議論を踏まえてまとめてみます.

メリット

  • 形式的証明可能性: 数式(仕様)とプログラムの対応関係が強く,仕様書とプログラムの相互変換が容易で,正しさを論じやすい
  • モジュラー性・結合性・等式推論: 小さな関数の合成でプログラムを組み立てられ,リファクタリングや最適化を等式的に行える
  • 参照透明性: 純粋関数は同じ入力に対して常に同じ出力を返し,テスト・デバッグ・並列化が容易になる
  • 静的型チェック: 実行前にバグを検出できる. 強力な型推論により型注釈の負担も軽減される
  • 高速性: コンパイラ方式のため一般に実行が速い. 副作用が無いぶん並列計算にも適する

デメリット

一方でHaskellには以下のようなデメリットもあり,常にあらゆる用途で最適とは限りません.

  • 学習コストが高い: モナドや型クラスなど,初学者にとって馴染みのない概念を習得する必要がある
  • エコシステムの規模: 機械学習・データ分析・Web開発などの領域では,Pythonの方がライブラリが充実している
  • 副作用の扱いが煩雑: Pythonなら素直に書けるI/Oやグローバル状態も,Haskellではモナドなどの仕組みを通す必要がある
  • 性能の予測が難しい: 遅延評価により,メモリ使用量や実行性能が直感と一致しない場合がある(スペースリーク等)
  • コミュニティ規模: 困ったときの情報量(Stack Overflow,日本語記事等)はPythonに比べて少ない

AIによるコーディング支援と静的型付け言語の再評価

近年は ChatGPT, Claude, GitHub Copilot, Cursor などの AI によるコーディング支援が急速に普及しています (エージェントの導入と操作は共通資料 コーディングエージェントの利用を参照してください). これは, Haskell のような「学習コストが高い」とされてきた言語にとって二重の追い風になりつつあります.

一つ目は 学習ハードルの低下 です. 従来は日本語情報の少なさや独特の概念(モナド, 型クラス等)の理解が初学者の壁となっていましたが, AI に質問しながら進めることで, その障壁はかなり下がってきています. 分からないエラーメッセージを貼り付けて解説してもらう, 書きたい処理をまずAIに書かせて読み解く, といった学習スタイルが現実的になりました.

二つ目は 静的型付け言語としての安全性の再評価 です. AI が生成したコードは一見正しそうに見えても,実は細かい型の不整合や前提の崩れを含んでいることがあります. 動的型付け言語ではこれらが実行時まで発覚しないことが多いのに対し, 静的型付け言語ではコンパイラが自動でチェックしてくれるため, AIが書いたコードを安全に使うための第一関門 として機能します. この観点から, Haskellやその他の静的型付け言語が AI 時代に改めて注目されています.

三つ目は, モジュラー性によって,AIによる作業の分割,並列化が可能になる点です. 純粋関数中心で副作用の境界が明確な設計では,「この関数を実装する」「この型定義を拡張する」といった単位でタスクを切り出しやすく,複数の AI エージェントに同時に担当させる運用と相性が良くなります. 各モジュールの入出力が型として明示されているため,統合時にも「どこが契約違反か」をコンパイラが即座に示してくれます. その結果,並列に生成したコードを安全に組み合わせやすくなり,大規模開発での AI 活用効率を高められます.

実世界でのHaskell

Haskellは研究目的で設計された背景をもつため,「学術的で実用性に乏しい」という先入観を持たれがちですが,実際には企業や OSS プロジェクトで広く利用されています. ここでは代表的な事例を紹介します.

企業での利用

ブロックチェーン

OSSツール


Haskell は Python のような「どこでも使われる」汎用言語ではありません. しかし,強い型安全性や厳密性が要求される領域(金融,ブロックチェーン,コンパイラ基盤,大規模バックエンド)で着実にユーザを獲得しています. 本講義で扱う「代数的に考える」習慣は,まさにこれらの領域で価値を発揮するものです.

その他の関数型言語

関数型言語はHaskellだけではありません. それぞれ異なる思想・用途で発展してきた言語があり,用途や好みに応じて選択肢となります.

これらの言語はそれぞれ異なる特徴を持ちますが,関数による合成, 不変性, 代数的データ型, パターンマッチ といった関数型の基本的な道具は共通しています. Haskellで学んだ考え方は,これらの言語にも(程度の差はあれ)応用できます.

Rust の状況

関数型言語の周辺で,近年最も注目を集めているのが Rust です. Rustは厳密には「関数型言語」に分類される言語ではありませんが, 関数型言語の長所を強く取り入れたマルチパラダイム言語 として, 関数型コミュニティからも強い関心が寄せられています.

Rustの主な特徴は以下の通りです.

Rustは, 長年 Stack Overflow の「最も愛されているプログラミング言語」ランキングで首位を取り続けるなど, 開発者からの評価が非常に高い言語です. 近年では Linux カーネルへの採用や, Microsoft, Google, Amazon, Meta 等の大企業での採用拡大も進んでいます.

Rust が関数型言語として注目される理由は, 「Haskellのような強い型システムと代数的データ型の恩恵を, 実用的な速度と豊かなエコシステムで享受できる」 点にあります. 純粋関数型ではない, 遅延評価ではない, モナドを使わないといった違いはありますが, 関数型的な設計指向を持つ現場では Rust が有力な選択肢になっています.

一方で Rust は, 所有権・借用チェッカ(borrow checker)との戦いが学習曲線の大きな山となっており, Haskell 以上に「書き始めの難しさ」を訴える声もあります. これもまた, AI支援の普及によって徐々に緩和されつつある領域です.

本講義の主題はHaskellですが,Haskellで学んだ代数的な考え方や型システムの感覚は, Rust をはじめとするこれらの近縁言語を学ぶ際にも強い土台となります.

いずれの言語が優れているかは目的次第です. 試行錯誤しながらデータ分析を行うならPython,厳密性や保守性を重視するシステムならHaskell,というように,言語の特徴を理解して使い分けるのが現代的な姿勢といえます. また,これまでにHaskellなどの関数型言語のメリットは,Pythonなどの手続き型言語にも取り入れられており,それらの機能を理解する上でも異なるパラダイムの言語を学ぶ意義はあります. 本講義ではあえてHaskellを扱いますが,それはHaskellが常に最良の選択肢だからではなく,Haskellを学ぶことで プログラミングの捉え方そのものが広がる からです.


  1. Peter Seibel, Coders at Work, Apress, 2009, pp. 262-264.↩︎

ce0f13b2-4a83-4c1c-b2b9-b6d18f4ee6d2