推し言語機能 Racket編

この記事はGMOペパボエンジニア Advent Calendar 2023 🎅会場の19日の記事です!

昨日はyagijinさんのReactやってる人向けのSwiftUI入門でした。 Swiftに興味があるReact信者の僕のために書いてくれたのかと錯覚しました。これを期にSwift UI入門しようと思います。 Swift UIは双方向バインディングを採用しているとのことなので、Vueとの類似もありそうですね。

Reactを書いている時間は癒しの時間です。ところでReactは関数型言語からインスパイアされた機能が多いですよね。今日の記事はそんな関数型言語の中でも僕の好きなRacket言語の記事です。僕が一推しするRacketの言語機能を紹介します。

好きなRacket言語の言語機能を紹介します。ざっくりとした紹介なので、ここでの知識を人に話したりプログラミングで活用する前に、節々で参照する公式ドキュメントを参照してもらえると嬉しいです。

ここでいう契約とは、関数の入出力の性質を関数定義の際に宣言しておくことで、関数を呼び出しを実行する際に入出力の値を検証し、違反していた場合にエラーを投げる言語機能のことです。余談ですが、契約と型は対応する概念です。契約の検証は実行時に行いますが、型検査はコンパイル時に行います。静的に型をつける言語では関数定義の際にその型を宣言することで、関数呼び出しのあるコードの入出力の方を検証し、違反する呼び出しを特定します。 ContractのRacket Guideを貼っておきます。https://docs.racket-lang.org/guide/contracts.html

ここでは https://docs.racket-lang.org/reference/function-contracts.html の例をお借りして説明します。

> (define/contract (maybe-invert i b)
    (-> integer? boolean? integer?)
    (if b ( -i) i))
> (maybe-invert 1 #t)
-1
> (maybe-invert #f 1)
maybe-invert: contract violation
  expected: integer?
  given: #f
  in: the 1st argument of
      (-> integer? boolean? integer?)
  contract from: (function maybe-invert)
  blaming: top-level
   (assuming the contract is correct)
  at: eval:2:0

冒頭の define/contract で始まるS式では、関数定義をしつつ、その関数の契約を宣言します。二つの引数 i, b をとる関数 maybe-invert を定義していて、bがtruthyなら-iを返しbがfalthyならiを返す関数として実装しています。この関数の契約は(-> integer? boolean? integer?) と宣言されています。これはinteger?を満たす値とboolean?を満たす値を引数に取り、integer?を満たす値を返す関数である、と読みます。

(maybe-invert 1 #t) という関数呼び出し式では第一引数に 1, 第二引数に #t を渡しているので入力に関する契約を満たしていて、返り値は-1になるので出力に関する契約も満たしています。そのためエラーが出ずに何事もなく計算結果の -1が表示されます。

一方で、 (maybe-invert #f 1) という関数呼び出しでは引数の順序を間違えて渡しているようです。第一引数にfalseを表す #f を渡しています。#finteger? を満たさない(そういうふうに integer?が定義されている)のでRacket処理系は契約に違反している旨をエラーとして表示しています。

こういうのが契約です。ちゃんと確認していませんが、PHPで型注釈を書いた際にも実行時の検査が行われるそうなのでPHPは契約を言語機能としてサポートしているといえそうです。他にはD言語も契約をサポートします。

入力値のバリデーションはコードを書くときに当たり前に行う作業ですが、それをシステマチックに行うためのフレームワークを言語が提供してくれるのは魅力的だと思っています。また、契約は型システムや漸進的型つけとも密接に関わりがある楽しい概念です。

次はnamed-letです。これについても公式ドキュメントを貼っておきます。https://docs.racket-lang.org/reference/let.html#%28form._%28%28lib._racket%2Fprivate%2Fletstx-scheme..rkt%29._let%29%29

RacketやSchemeなどのLisp系言語で勉強しているとwhileやforは習いませんが、再帰を習います。例えば整数のリストを受け取って、その和を返す関数sumを定義するにはこんな感じで書きます。

> (define (sum lst)
          (if (nil? lst)
              0
              (+ (car lst) (sum (cdr lst)))))
> (sum '(1 2 3 4 5 6 7 8 9 10))
55

nil?はリストが空を判定し、carはリストの先頭要素をとる関数で、cdrはリストの先頭を除いた部分をとる関数です。先頭の値とそれ以降の和をたすことで、リスト全体の和を得ています。

このように再帰があればループはかけるのですが、関数を定義してその直後にその関数を呼び出す、そしてその後その関数は使わない、みたいなケースがしばしばあります。そういうときに役立つのがnamed-letです。以下のように再帰関数の定義と関数呼び出しを同時に行うことができます。

> (let sum ((lst '(1 2 3 4 5 6 7 8 9 10))
       (if (nil? lst)
           0
           (+ (car lst) (sum (cdr lst))))))
55

再帰関数を使ってループを書くことでループの中での再代入を避けることができます。そのためループ不変条件を把握するのが楽になります。そしてnamed-letを使うことで、不要な関数定義を省くことができます。スコープにある変数は少ないほど嬉しいです。ここだけで使う再帰関数なんだということが一目でわかります。あと書いてみるとわかるのですが、named-letを書くととても気分がいいです。

最後は動的束縛です。emacs lispの変数束縛は動的だということで有名ですし、最近だとReactのContextが動的束縛っぽいかなと思います。やはり公式ドキュメントのリンクを貼っておきます。https://docs.racket-lang.org/guide/parameterize.html

誤解を恐れながらいうと、環境変数みたいなやつをコードの中で設定できる機能です。公式ドキュメントの例をそのまま貼り付けます。

> (parameterize ([error-print-width 5])
    (car (expt 10 1024)))
car: contract violation
  expected: pair?
  given: 10...
> (parameterize ([error-print-width 10])
    (car (expt 10 1024)))
car: contract violation
  expected: pair?
  given: 1000000...

REPLで2回プログラムを実行しています。(expt x y)xy 乗です。それのcarを取ろうとしています(carはリストの先頭要素を返す関数でした)が、(expt x y)の計算結果は数値であってリストでないのでcarの契約に違反します。そのためエラーメッセージが表示されています。今回フォーカスしたいのは動的束縛です。parameterizeerror-print-widthの値をそれぞれの実行で 510に指定しています。その結果表示されるエラーメッセージの幅が5になったり10になったりしています(10...1000000...)。おそらくエラーメッセージを表示する関数の中で error-print-widthが参照されているのでしょう。

このように関数を呼び出すタイミング側でその振る舞いを変えられるのが動的束縛の旨みです。関数の引数で渡す必要がないので、動的変数を参照する関数と設定する関数の間の関数たちが余分な引数を取らなくても良いわけです。

グローバル変数を使ってもこのような引数を介さない設定はできますが、動的束縛を使う方が衛生的です。例えば一つのプログラムの中でグローバル変数の値を変えたい場合、グローバル変数の値を書き換えることになります。これは悪名高い可変なグローバル変数を使うことを意味します。可変なグローバル変数は無関係に見えるプログラムの実行順序がクリティカルにプログラムの振る舞いを左右するのでよくないです。

一方で動的束縛では関数呼び出しごとに値を設定するため実行順序のことは気にしなくて良いです。値を設定した呼び出しの範囲下ではそれが反映されるし、その範囲外ではその設定は無効化されます。このように衛生的に、かつ疲れない形で広い範囲で参照する値を設定できることがグローバル変数と比較した際のメリットだと思います。

動的束縛される変数を参照する場合、呼び出し側の不慮の事故によって変数の定義もれでプログラムが正常に動作しないかもしれません。環境変数を設定し忘れるとアプリケーションが動かないのと同じです。こういう事故は実行する前、例えばコンパイルしたり型検査のタイミングで気がつけると嬉しいですよね。

Racketでこの課題を解決できるかは知らないのですが、エフェクトハンドラとエフェクトシステムを使えば型検査の中で解決できます! その説明をしたい気持ちが溢れているのですが、そろそろ日を跨ぎそうなので興味がある方は僕に声をかけてくれると嬉しいです。

プログラミング言語の言語機能、いいですよね。言語設計者が思う表現のベストプラクティスが詰まっていて触れるたびに嬉しくなります。

明日のアドベントカレンダーは冷静沈着なTepiさんがTextのJetpack Composeで画像表示した話を書く予定とのことです。 Jetpack Composeは名前しか聞いたことがないので、新しい概念を見られそうで楽しみです!

それでは良いクリスマスを!