「関数型言語で学ぶプログラミングの基本」という同人誌を書いてみて

@asya_aoi1049 on Wed Dec 21 2022
7.4 min

目次

はじめに

2022/11/20に開催された第七回 技術書同人誌博覧会に出展する同人誌を執筆した。

目的は色々あるが、一番は自身の知識の整理が大きい。

ここでは、書籍内で重要だと(個人的に思う)概念及び執筆にあたって気をつけた点をピックアップする。

なぜ型が必要なのか

Chapter 2 の序文のとおりであるが、「1 + “ほげ”」のような無意味な計算を未然に防ぐためである。

このようにデータの種類を確認することを 型チェック と言う。
また、無意味な計算によって発生するエラーを 型エラー と言う。

基本的データとして、整数(int)、実数(float)、文字(char)、文字列(string)、真偽値(bool)が存在する他、
戻り値を返す必要がない場合に用いるユニット値(unit)が存在する。

いくつかのプログラミング言語では、副作用を引き起こす関数の戻り値としてユニット型を返したり、
引数を必要としない関数の引数の型としてユニット型が用いられる場合がある。

変数と関数におけるスコープについて

特定のプログラミング言語に限った話しではないが、変数や関数定義を参照できる範囲が スコープ であり、
これを必要最低限の範囲に制限することで意図しない参照やミスを防げる。

OCamlではinキーワードによってスコープをin以降に限定できる。

utop # let x = 3 in x + x ;;
- : int = 6

(* x は上記の x + x でのみ有効 *)
utop # x ;;
Error: Unbound value x

プログラミングする上で、スコープは常に意識したいポイントだと考えている。
といってもOCamlであれば、自然とスコープが適切に限定されるような記述になるとは思う。

参照透過性について

いつも同じ値を持ち続ける性質を 参照透過性 と言う。

なお、同一の変数名に対して異なる値を設定できるからと言って「参照透過性がない」とは言えない。
OCamlでは、同じ名前であっても定義が異なる変数によって古い定義を隠す シャドーイング が行われるからである。

utop # let a = 1 ;;
val a : int = 1

utop # let a = 2 ;;
val a : int = 2

そのため、自身が扱うプログラミング言語の仕様は公式ドキュメントを参照し確認するべし。

型推論について

関数の内容やその周辺情報及び文脈から引数や関数の型を自動的に推測する仕組みを 型推論 と言う。

型推論はプログラマを強力にサポートしてくれるが、完全ではない。
型システムが苦手とする記述も存在するため、その場合は明示的に型を記述する必要がある。

私個人としては可能な限り型推論に任せ型を明示的に書かないスタンスを取るが、
プログラミング言語によっては型推論が行われる場合であっても明示的に書くべきという流派もあるので、
そこはチームや当該プログラミング言語界隈に合わせていくのが丸い(と思う)。

カリー化関数と非カリー化関数

以下のように2つの引数を受け取ってその差を返す関数を考えた時、関数の型が「int -> int -> int」となっていることが分かる。

utop # let diff x y = x - y ;;
val diff : int -> int -> int = <fun>

これは、(fun x -> (fun y -> x - y))と同等であり(関数の型が一致する)、
ここで任意の型をtとすると t1 -> t2 -> t3 のように表現できる。

OCamlでは、このようなスタイルの型を持つ関数をカリー化関数と言う。

上記の表現は、複数の引数を取る関数を、たった一つの引数を取る関数で表現した形 である。

一方、複数の引数を取る関数で引数の一部を省略できない関数を非カリー化関数と言う。

TypeScriptで先程定義したdiffをカリー化表現及び非カリー化表現で表すと次のようになる。

// Curring
const diff = (x : number) => (y : number) => x - y;

// Un Currying
const diff = (x : number, y : number) => x - y ;

OCamlの場合、diff 3 1と書けば2が得られ、diff 3と書けば「引数を1つ取り整数型を返す関数」が得られる(部分適用と言う)。
TypeScriptや他n一部のプログラミング言語ではそうはいかない(それ自体が悪いとは言っていない)。

カリー化によって得られる恩恵はいくつかあるが、個人的には「より一般化された関数をベースとして、引数に応じた別の関数を作る」ことだと思う。

例えば下記のようにリスト[1; 2; 3; 4; 5]に対して3との差を要素として持つリストを容易に記述できる。
引数を省略できない関数ではこうはいかない。

utop # List.map (diff 3) [1; 2; 3; 4; 5] ;;
- : int list = [2; 1; 0; -1; -2]

執筆にあたって気をつけたこと

誰かのための本を書くという行為は初めてだったので分からないことだらけだった。

手探りながらも気にした点について列挙してみる。

  • だれに向けて書くのかを明確にする
  • 何を伝えたいのかを明確にする
  • 誤った情報を書かないためにソースを明らかにする

だれに向けて書くのかを意識すると、表現にこだわるようになった。
本書は初学者向けなので、学術的な表現を控えつつ誤解を生まない程度の表現になるように執筆したつもりだ。
例えば、レビューで議論になった点であるが、float型を実数と表現して良いのかどうか等・・・

本書で目指したのは、OCamlの機能を用いて関数型プログラミング言語の文脈で目にする用語の定義及び言語毎の違いだ。
これについては感想を待つしか無い。

誤った情報を書かないために可能な限り公式ドキュメントや支持されているサイト等を参照した。
また、OCamlにおける文脈で用いられる概念なのか一般的な文脈と共通の概念なのか等には細心の注意を払ったつもりだ。
※もし書籍の内容に誤りがあれば教えてほしい

まとめ

執筆活動を通して多くの気づきを得ることができた。

  • 自分のプログラミング言語に対する理解がどれだけか
  • 一般的な概念・用語とプログラミング言語における概念・用語の差異
  • 執筆における心構え

これらの知見を今後も活かしていきたい。

日別に記事を見る