On this page:
3.1 Why Typed Racket
3.2 require/  typed
3.3 Typed Function
3.4 assert
3.5 Type Estimation
6.8

3 Typed Racket, How to

3.1 Why Typed Racket

プログラミング言語は静的(static)な型付けを行うものと動的(dynamic)な型付けを行うものに分けられますが、そもそもなぜデータに「型」を持たせるのでしょうか。

それは計算が速くなるからです。

Common LispやSchemeなどのLISPでは「数」を抽象的に扱うため、ある数を扱うのに必要なメモリが容易には定まりません。例えば、整数を表すのに必要なメモリと複素数を表すのに必要なメモリは異なりますので、行列のように複数の数をまとめて処理する場合も、メモリのアドレスを容易に計算することができず、処理が遅くなります。さらに、全く別のデータも格納できるように動的な処理を許せば、ますます実行時の処理が増えることになります。

そこで、事前に型を固定して計算を速くしようというのが静的な型付けの最大の理由です。

ただし、静的な型付けには別のメリットもあり、型の違反を事前に防ぐことができます。例えば、文字列を数に変換するstring->numberは数に変換できない文字列を受け取った場合は、#fを返し、エラーにはなりません。一般にこの処理は便利ですが、受け取った結果をさらに計算に回す場合、#fにはどのような四則演算も行えないので、その時点でエラーになります。

静的な型付けを徹底する場合、四則演算を行う手前で値が数であることを保証し、四則演算に回すことになります。このように、より厳格かつ頑健なプログラムを作ろうとする場合、「型」に注目するのはいいアプローチです。

Racketには「コントラクト」、すなわち契約というシステムが組み込まれており、型付けを行わずに必要な箇所だけコントラクトを設定しておくことが可能なので、頑健なプログラムを作るという目的であればコントラクトを使うことも検討してください。

現実には、Mathのような数値計算ライブラリがTyped Racketで書かれており、ライブラリを呼び出す側もTyped Racketにすることで速度的なメリットが得られることが型付けの主な理由です。ただし、Racketの特徴として、Typed Racketのライブラリを普通のRacketから呼び出すこともできますので、速度面を気にしなければ普通のRacketでも使うことができます。

3.2 require/typed

Typed Racketで書かれていないモジュールをTyped Racketから呼び出すには以下のようにrequire/typedを使います。私はCSVファイルをよく扱いますが、csv-readingは普通のRacketなので、csv->list関数を以下のようにインポートします。

(require/typed csv-reading
               [csv->list (-> Port (Listof (Listof String)))])

->というオペレータは、関数の引数と返り値の型を示すものです。一番最後の値が返り値になり、それ以外は引数と見なされます。csv->listは引数として入力ポートPortを取り、文字列の二次元リスト(Listof (Listof String))を返すので、このように指定してインポートします。

3.3 Typed Function

関数については、関数の定義の直前に型を指定します。例えば、数の配列について、指定の列をベクトルとして抽出するような関数は以下のように記述できます。(配列はmath/arrayを使用し、行列の場合はmath/matrixを使用します。

(: mcol (-> (Array Float) Index (Array Float)))
(define (mcol arr n)
  (array-flatten (array-slice-ref arr (list (::) (:: n (fx+ n 1))))))

:というオペレータが関数の型指定であることを示し、次に関数の名前を記述します。->以降は前節と同じです。

Indexとは整数Integerの中で配列の要素数までの数を示します。数値計算の場合、数値自体の型はFloat(C言語のdoubleに相当)に統一しておく方がシンプルで速いため、配列は(Array Float)という型になります。

fx+は整数、特に固定長整数Fixnumの範囲内での計算を高速に行う加算演算子です。racket/fixnumライブラリで定義されています。

3.4 assert

Racketの型システムは高機能であり、複数の型を指定することができます。その例が先に挙げたstring->numberのようなものであり、この関数の返り値の型は(U Complex False)となっています。これは、複素数(つまり数)か#fということですが、あとに続けて計算を行う場合、数であることを保証する必要があります。数であることを保証するには、数でない場合にエラーを発生させ、プログラムを止める必要があります。そこで、string->numberを以下のように拡張します。

(: string->number* (-> String Float))
(define (string->number* str)
  (assert (string->number (string-trim str)) flonum?))

assertは型を保証する仕組みで、特定の型にマッチしない場合はエラーを起こす代わりに、その返り値が特定の型であることを保証します。私が使う数値計算では複素数は扱わず、浮動小数点までですから、flonum?という述語で型を検査します。

なお、Schemeでは一般に空白を含む文字列を数に変換することができませんので、string-trimを使って空白を除去してから変換します。

3.5 Type Estimation

Typed Racketの場合、関数を適切に型付けしておけば型推論が働きますので、関数の返り値を束縛する変数や定義については型指定が不要です。

ただ、全く気をつけなくて言い訳ではありません。エラーになりやすい変数の型として、Fixnumへの加算があります。Fixnumの最大の値に1を足すとそれはFixnumの範囲を超えたIntegerになりますので、Fixnumに1を加算した値をFixnumと推論することはできません。このような場合はassertを使ってFixnumであることを保証するか、Integerを使うことになります。

CやJavaに慣れているとあらゆる場所で型の指定を行いますが、現代的な言語では型の指定の影響がソースコードに出てくる部分は意外と少ないです。