haskellに入門して数ヶ月経ったので関数の美しさについて語ってみる

2022/02/20に公開

前置き
関数型言語の勉強がしたくて、haskellに入門しました。ノロノロとですが、少しずつ勉強を重ね、今は競プロの練習言語として使っています。実務やアプリ作成での経験はありません。それを踏まえた上での感想として受け止めてね。

関数型言語と手続き型言語やオブジェクト指向言語の違い

よく関数型言語は変数再代入できないとか、すべてがイミュータブルであるみたいな違いを意識しがちだけど、僕はどちらかというと文と式という構成要素の違いが、プログラミングのしやすさにかなり影響を与えていると思う。

関数型言語はすべてが式である

多分文もあると思いますが、理解のしやすさのためにここでは「関数型言語はすべてが式である」と言い切ってみます。

関数型言語ではif文もswitch文もありません。すべてが式なので、if式やcase式となります。

そもそも式とは何なのかというと、
3 + 4
の「3」も「+」も「4」も式になります。またこの「3+4」という式を別の式に入れ込むこともできます。
2 * (3 + 4)
数学の世界において式は、デコレーターパターンなのです。

デコレーターパターンとは、ファイルとフォルダの関係です。ファイルはどのフォルダにも移動できますし、フォルダ自体も他のフォルダの子要素になることができます。HTMLのタグ要素もデコレーターパターンなので、構造を入れ替えたり、交換したりできます。デコレーターパターンはそれだけでかなり強力です。ボトムアップ式に構成要素をつくり、それを組み合わせることができます。組み合わせの数は組み合わされる側の要素が多くなれば爆発的に多くなります。多様性をもたせることができます。また、各要素は交換可能なので、リファクタリングや変更がしやすくなります。

ただし、プログラミングの世界は数学と違って、数字以外にも文字列などがでてきます。「+」の演算子が数字の世界だけを想定していた場合、もし言語ユーザーが文字列同士の「+」をしたとき、エラーになります(haskellでは式に「+」演算子を適用したときどんな処理をするかは型クラスで定義します。「+」に対応した型クラスを継承していれば足し算が可能です)。つまり、プログラミングの世界では式は厳密にはデコレーターパターンではなく、演算子の対応する型の範囲ないで交換可能です。ただし、デフォルトでデコレーターパターンを実現しやすいというのは、手続き型言語やオブジェクト指向言語とくらべて十分に強力だと思います。

話はそれますが、「2 + "2"」のような式はおかしいのですが、いくつかの言語では可能です。

文、式、クラスと文脈スイッチが必要なオブジェクト指向言語

オブジェクト指向言語へのちょっとした不満です。不満ポイントは3つあります。

  • 文、式、クラスと文脈スイッチが必要
  • クラスは交換可能につくれるがクラスの中身は?
  • 「継承よりも委譲をつかう」というプラクティス自体がクラスの文脈だけで完結できていない

文、式、クラスと文脈スイッチが必要

関数型言語ではミクロからマクロまですべてが式なので、抽象レベルから実装レベルまでシームレスにつながっていきます。明確にどこから抽象レベルで、どこまでが実装レベルなのかはわかりません。

オブジェクト指向言語では、文と式とクラスの3つの文脈がまじります。これは手続き型言語の上にオブジェクト指向のクラスという考え方を取り入れたからですね。僕がモヤッとしてるのは、文と式という文脈中に突然new演算子というクラスの文脈が現れることです。そこでクラス思考に頭を切り替えないといけません。またクラスは実装レベルは文と式なので、クラスの中身を記述するときはまた文と式思考に頭を切り替えないといけません。

クラスは交換可能につくれるがクラスの中身は?

クラスは関数型の式と同様にインターフェースを使えば交換可能に作れます。クラスは実装レベルでは文と式に依存しています。「式と式」と違って「文と式」「文とクラス」は交換可能でありません。クラスは外の世界も内側の実装も文と式で構成されており、すべてがクラスではありません。リファクタリングが辛いのはこの性質が有るからだと思います。

「継承よりも委譲をつかう」というプラクティス自体がクラスの文脈だけで完結できていない

オブジェクト指向のベストプラクティスの一つに「継承よりも委譲を使う」というものがあります。継承はクラスの文脈の1つで、ベースとなるクラスから派生したクラスを作る機能です。対して委譲はクラスの文脈ではなく、依存に渡して内部でそれを使うというプラクティスの一つです。結局クラスの文脈のみで完結せず、内部の実装レベルの文と式を使う必要があります。

依存をコンストラクタに入れるクラスと、引数にいれる関数

関数はマイクロクラスなのではと最近思います。マイクロクラスはサービスにおけるマイクロサービスを比喩して今名付けました。

クラスは依存を一度コンストラクタにいれ(依存注入DIと呼ぶ)、それを各メソッドで取り回します。対して、関数は依存を引数にいれすぐに使います。

関数型の依存注入をするとテストのしやすさというメリットがあります。

クラスのテストはコンストラクタに依存をいれるというセットアップのフェーズを必要とします。

test('成人しているかどうか', () => {
  const user = new User({
    name: '太郎',
    age: 17,
  });
  
  expect(user.isAdult).toEqual(false);
})

それに対して、関数のテストは直接できます。またその関数に不要な余分な依存も持ちません。

test('成人しているかどうか', () => {
  expect(isAdult(17)).toEqual(false);
})

このメリットにより、関数型言語はインタプリタ上でテストしやすいです。またdoctestという関数へのコメントでテストするという手法がよく使われます。

https://hackage.haskell.org/package/doctest

以下はフィボナッチ数列のdoctest。

module Fib where

-- | Compute Fibonacci numbers
--
-- Examples:
--
-- >>> fib 10
-- 55
--
-- >>> fib 5
-- 5
fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)

関数型言語界隈に人が増えてほしいなぁ

と思いつつ、今回入門して難しさを感じました。

理由はどうしてもモナドという中ボスと序盤のステージでエンカウントするからです。街で剣の振り方を覚えた初心者冒険者が、さぁたびに出るぞと街を出た瞬間に中ボスと出会い惨敗すれば、もう冒険しようと思わないでしょう。関数型言語界隈はここらへんどうにかしないと一向に人は増えないだろうなと思います。

Discussion