TypeScript使いがHaskellに入門して、プログラミングパラダイムの「解像度」が上がった話
こんにちは、ココナラで Web エンジニアをしている慕狼ゆに (しのがみゆに) (@yuni_shinogami) です。
こちらは株式会社ココナラ Advent Calendar 2025 15 日目の記事です。
普段は業務で TypeScript を書いていますが、今回は技術的な How-to ではなく、「あえて Haskell に入門してみた」という個人の学習記録を書きたいと思います。
「なぜ今さら Haskell?」と思われるかもしれませんが、普段 TypeScript を書いている人間が、Haskell を学習することで、どのように普段使っている言語(JavaScript/TypeScript)への理解を深めたか、その思考のプロセスを共有できれば幸いです。
きっかけは「fp-ts」への苦手意識
事の発端は、副業で fp-ts というライブラリに触れたことでした。 fp-ts は、TypeScript でより厳格な関数型プログラミングを行うための抽象化を提供するライブラリです。
使い始めてすぐに、壁にぶつかりました。 Option や Either といった型定義、pipe や flow を使ったデータフロー、そしてドキュメントに当たり前のように登場する Functor(関手)や Monad(モナド)といった数学的な用語たち……。
見よう見まねで動くコードは書けるものの、「なぜここで map を使うのか?」「なぜ flatMap が必要なのか?」という背後にある理屈が直感的にイメージできず、ただ「お作法」としてパズルを解いているような感覚に陥りました。
これを使っている時、ふと 「自分は関数型プログラミングをよくわかってないかもしれない」 という強い不安を感じました。
もちろん知識として、関数型プログラミングには以下の概念があることは知っていました。
- 参照透過性
- 純粋関数
- 関数を第一級の値として扱う
このうち、純粋関数については、「むやみに状態を変えず、副作用を減らす」という良いコードを書くためのルールとして個人的に日頃から意識していました。
しかし、それ以外の「参照透過性」や「関数を第一級の値として扱う」といった概念については、言葉の意味は知っているものの、それらがどう繋がり、なぜ重要なのかという「関数型の思想(パラダイム)」としての実感までは持てずにいました。 あくまで「部分的なテクニック」としてしか捉えられていなかったのです。
「関数型を真に理解していれば、この fp-ts の書き方は自然に馴染むはずだ。でも馴染まないということは、自分はまだ理解が浅いのではないか?」
そう感じたことが、今回の学習のきっかけでした。
そうだ、Haskell をやろう
意味を理解しているだけでは実感を得られない。どうすれば良いか。 そこで思いついたのが、「関数型言語の古典、Haskell を触ってみれば何かわかるかもしれない」 という単純な発想です。
以前読んだ『7つの言語 7つの世界』でも関数型の代表例として扱われていましたし、界隈でよく聞く「エンジニアなら一度は Haskell に触れておくべき」という言葉も気になっていました。
しかし何よりの理由は、中途半端に手続き型も書ける言語ではなく、強制的に「関数型」の作法で書かざるを得ない厳格な環境に身を置いてみたかったからです。逃げ場のない環境なら、身体で覚えるしかないと考えました。
そこで今回は「Haskell を実務レベルで書けるようになること」は目的にせず、あくまで「関数型を理解するための題材として Haskell を使う」というスタンスで学習を始めました。
学習にあたり、以下の 3 冊を参考文献として読みつつ、LLM(AI)と壁打ちをしながらコードを書いていくスタイルをとりました。
- 『Clean Architecture』
- 『7つの言語 7つの世界』
- 『すごい Haskell たのしく学ぼう!』
実践:FizzBuzz で文化の違いを知る
言語の文化を知るには、定番の FizzBuzz を書いてみるのが一番です。
今回は、学習対象の Haskell、普段書いている TypeScript(手続き的・fp-ts)、そして最近触り始めた Ruby でも書き比べてみました。なお、本記事で実行した各言語・ライブラリのバージョンは以下の通りです。
- Haskell: GHC 9.6.7
- TypeScript: 5.9.3 (実行環境: ts-node 10.9.2 / ライブラリ: fp-ts 2.16.11)
- Ruby: 3.2.3
Haskell の場合
「純粋関数(計算)」と「IO アクション(出力)」が明確に分かれています。ガード(|)を使った分岐が特徴的です。
-- 純粋なロジック部分(副作用なし)
fizzbuzz :: Int -> String
fizzbuzz n
| n `mod` 15 == 0 = "FizzBuzz"
| n `mod` 3 == 0 = "Fizz"
| n `mod` 5 == 0 = "Buzz"
| otherwise = show n
-- IOアクション(副作用あり)
main :: IO ()
main = mapM_ (putStrLn . fizzbuzz) [1..100]
TypeScript (手続き的)
比較対象として、あえて「手続き型(命令型)」のスタイルで書いています。 for 文を使い、処理の手順を上から下へ記述する、最も直感的な書き方です。
const fizzBuzzProcedural = () => {
for (let i = 1; i <= 100; i++) {
if (i % 15 === 0) {
console.log("FizzBuzz");
} else if (i % 3 === 0) {
console.log("Fizz");
} else if (i % 5 === 0) {
console.log("Buzz");
} else {
console.log(i.toString());
}
}
};
fizzBuzzProcedural();
TypeScript (fp-ts) の場合
fp-ts の pipe を使い、データが流れるように処理する「パイプライン処理」のスタイルです。 値(1 から 100 の配列)があり、それを変換(map)し、最終的に出力するというデータの流れが可視化されています。
import { pipe } from 'fp-ts/function';
import * as A from 'fp-ts/Array';
// 純粋な変換関数
const toFizzBuzz = (n: number): string => {
if (n % 15 === 0) return 'FizzBuzz';
if (n % 3 === 0) return 'Fizz';
if (n % 5 === 0) return 'Buzz';
return n.toString();
};
const main = () => {
pipe(
A.makeBy(100, (i) => i + 1), // [1, 2, ..., 100] を生成
A.map(toFizzBuzz), // すべてをFizzBuzz文字列に変換
A.map(console.log) // すべてを出力
);
};
main();
Ruby の場合
最近触り始めた Ruby でも書いてみました。 Ruby はオブジェクト指向ですが、ブロック構文や「if 文も値を返す(式である)」という特性を活かすと、非常にスッキリとした関数型ライクな記述が可能です。
(1..100).map { |n|
if n % 15 == 0
"FizzBuzz"
elsif n % 3 == 0
"Fizz"
elsif n % 5 == 0
"Buzz"
else
n.to_s
end
}.each { |str| puts str }
こうして並べてみると、同じ結果を出すプログラムでも、アプローチが全く異なることが見えてきます。
Haskell を触って面白かった「発見」
実際に LLM にコードを書かせたり、自分で修正したりする中で、いくつかの気づきがありました。
1. IO 型と副作用の可視化
一番の衝撃であり、面白かったのが 「IO 型」 です。
Haskell では、副作用(画面出力や DB 接続など)がある処理には IO という型がつきます。これにより、「副作用がある関数」と「純粋な関数」が型レベルで明確に区別されます。
「副作用とは何か」という曖昧だった概念を、IO 型の理解を通じて深めることができました。
私の中で、TypeScript における副作用の制御は、あくまで 「可読性を高めるためのお作法」 や 「きれいなコードを書くための美意識」 として自分自身に課していたものでした。しかし、Haskell ではそれが言語仕様として厳格に定義されており、強制力を持って突きつけられる点が大きな違いだと感じました。
2. JavaScript 自体に「関数型」は溶け込んでいた
Haskell で純粋な関数型プログラミングの概念を学んだ後、普段書いている TypeScript(およびその実体である JavaScript)を見返すと、それまで「便利な構文」として見ていた機能の背後にある思想が、くっきりと別個のものとして見えてきました。
-
関数を第一級の値として扱う :
const hoge = () => { ... }のように関数を変数に代入したり、コールバックとして引数に渡す書き方は、まさに関数を「値」として扱う思想そのものです。 - 関数合成(パイプライン) : 配列に対するメソッドチェーンは、小さな関数を組み合わせてデータを加工していく「関数合成」の概念がベースになっています。
// 小さな処理をつなげて大きな処理を作る
items.map(toPrice).filter(isAffordable);
- カリー化 : ライブラリの実装などで見かける「アロー関数が連続する書き方」は、Haskell では当たり前の「カリー化」そのものでした。
// 引数をひとつずつ適用する形
const add = (x) => (y) => x + y;
-
モナド (Monad) : Haskell で鬼門と言われる「モナド」も、「文脈(Context)を持った値を扱うための仕組み」 だと理解すれば、普段使っているものが実はモナド的なパターンだと気づきました。
-
Promise.then()チェーン → 非同期という文脈 -
Array.flatMap()→ 複数という文脈
-
これらは単なる「便利な機能」ではなく、JavaScript という言語の仕様の中に、関数型プログラミングの各概念が形を変えて深く溶け込んでいたのだと再認識しました。
「マルチパラダイム」という答え
ここで一つの疑問が浮かびます。
「TypeScript でも関数型っぽく書けるけど、これは本当に関数型プログラミングをしていると言えるの? 手続き的にも書けるのでは?」
そこでMDN Web Docsを確認してみると、JavaScript は 「マルチパラダイム」 であるという記述が冒頭にあります。オブジェクト指向もできるし、関数型もサポートしている言語なのです。
ここで、学習中に読んだ『Clean Architecture』の話が繋がりました。同書ではプログラミングパラダイムを以下の 3 つに大別しています。
- 構造化プログラミング
- オブジェクト指向プログラミング
- 関数型プログラミング
JavaScript(そしてそのスーパーセットである TypeScript)は、これらのパラダイムの歴史を経て、複数のパラダイムを取り入れて形成された言語だったのです。
Haskell という「純粋関数型言語」を体験したことで、逆に 「JavaScript がいかに柔軟にパラダイムを行き来できる言語か」 という解像度が上がりました。
おわりに
TypeScript 使いが Haskell に入門した結果、Haskell マスターにはなれませんでしたが、「プログラミングパラダイム」という視点を手に入れることができました。
ただ、正直に言うと、今回触れた内容は Haskell や関数型プログラミングという巨大な世界のほんの入り口に過ぎないとも感じています。 学習を進める中で、その理論の美しさと奥深さに触れ、まさに 「底なし沼」 のような広がりを肌で感じました。モナド、関手、圏論……まだまだ理解が及んでいない概念が山のようにあります。
ですが、今回の旅で「パラダイムの違いを楽しむ」という地図だけは手に入りました。 今後もこの沼に足を取られ……いや、浸かりながら、少しずつ関数型プログラミングへの理解を深めていきたいと思います。
新しい言語を習得したり、言語の特徴を考察するときに「これはどのパラダイム(思想)に基づいているのか?」という視点を持つことは、技術の習得や設計の理解において非常に有用だと感じています。 もし「今の言語の書き方にマンネリを感じている」という方がいれば、全く違うパラダイムの言語に触れてみてはいかがでしょうか。
おまけ
この話を友人にしたら、「関数型を知りたいなら Lisp を書こう!」 と言われました。
言われるがままに Lisp (ANSI Common Lisp) を書いてみましたが……
(labels ((fizzbuzz (n)
(cond ((> n 100) nil)
(t (progn
(format t "~a~%"
(cond ((zerop (mod n 15)) "FizzBuzz")
((zerop (mod n 3)) "Fizz")
((zerop (mod n 5)) "Buzz")
(t n)))
(fizzbuzz (1+ n)))))))
(fizzbuzz 1))
無事、大量のカッコに埋もれてしまいました。
現場からは以上です。
明日はゴローさんによる「単体テストと学派のはなし」です。
ココナラでは積極的にエンジニアを採用しています。
採用情報はこちら。
カジュアル面談希望の方はこちら。
Discussion