Skip to content

型システムを使って正しいコードを保証する

F#では型システムはあなたの味方であり、敵ではありません

C#やJavaなどの言語を通じて、静的型チェックには馴染みがあるでしょう。これらの言語では、型チェックは単純ですが粗く、PythonやRubyなどの動的言語の自由さに比べると煩わしく感じることがあります。

しかし、F#では型システムはあなたの味方であり、敵ではありません。静的型チェックをほぼ即座のユニットテストのように使えるのです。つまり、コンパイル時にコードの正しさを確認できます。

これまでの投稿で、F#の型システムでできることをいくつか見てきました:

  • 型とそれに関連する関数は、問題領域をモデル化する抽象化を提供します。型の作成が非常に簡単なので、与えられた問題に必要な型を設計するのを避ける言い訳はほとんどありません。また、C#のクラスとは異なり、何でもできる「何でもあり型」を作るのは難しくなっています。
  • よく定義された型はメンテナンスに役立ちます。F#は型推論を使うので、通常はリファクタリングツールを使わずに型の名前変更や再構築が簡単にできます。また、型が互換性のない方法で変更された場合、ほぼ確実にコンパイル時エラーが発生し、問題の追跡に役立ちます。
  • 適切に名付けられた型は、プログラム内での役割に関する即座のドキュメントを提供します(そして、このドキュメントが古くなることは決してありません)。

この投稿と次の投稿では、正しいコードを書くための補助として型システムを使うことに焦点を当てます。実際にコンパイルが通れば、設計通りに動作するようなデザインを作れることを示します。

C#では、考えることなくコンパイル時チェックを使ってコードを検証しています。たとえば、 List<string> を単なる List に置き換えたいと思いますか?あるいは、 Nullable<int> をやめてキャストを伴う object を使うことを強いられたいですか?おそらくそうではないでしょう。

しかし、さらに細かい粒度の型を持てたらどうでしょうか?さらに優れたコンパイル時チェックが可能になります。そして、これがまさにF#が提供するものです。

F#の型チェッカーは、C#の型チェッカーよりそれほど厳密というわけではありません。しかし、煩雑さなしに新しい型を作るのが非常に簡単なので、問題領域をより良く表現でき、有用な副作用として多くの一般的なエラーを避けることができます。

以下は簡単な例です:

// "安全な"メールアドレス型を定義
type EmailAddress = EmailAddress of string
// それを使う関数を定義
let sendEmail (EmailAddress email) =
printfn "%s にメールを送信しました" email
// 送信を試みる
let aliceEmail = EmailAddress "alice@example.com"
sendEmail aliceEmail
// 単純な文字列で送信を試みる
sendEmail "bob@example.com" // エラー

メールアドレスを特別な型でラップすることで、通常の文字列をメール固有の関数の引数として使えないようにしています。(実際には、 EmailAddress 型のコンストラクタも隠して、最初から有効な値だけが作成できるようにします。)

ここには、C#でできないことは何もありませんが、この1つの目的のために新しい値型を作るのはかなりの作業になるでしょう。そのため、C#では単に文字列をあちこちに渡すという怠惰な方法を取りがちです。

「正確性のための設計」という主要なトピックに移る前に、F#の型安全性が発揮される場面を他にもいくつか見てみましょう。些細ですが、クールな機能ばかりです。

printfを使った型安全なフォーマット

Section titled “printfを使った型安全なフォーマット”

ここで、F#がC#よりも型安全である方法の1つを示す小さな機能を紹介します。これは、C#では実行時にしか検出できないエラーをF#コンパイラがどのように捕捉できるかを示しています。

以下を評価して、生成されるエラーを確認してみてください:

let printingExample =
printf "整数 %i" 2 // OK
printf "整数 %i" 2.0 // 型が間違っています
printf "整数 %i" "hello" // 型が間違っています
printf "整数 %i" // パラメータが不足しています
printf "文字列 %s" "hello" // OK
printf "文字列 %s" 2 // 型が間違っています
printf "文字列 %s" // パラメータが不足しています
printf "文字列 %s" "he" "lo" // パラメータが多すぎます
printf "整数 %i と文字列 %s" 2 "hello" // OK
printf "整数 %i と文字列 %s" "hello" 2 // 型が間違っています
printf "整数 %i と文字列 %s" 2 // パラメータが不足しています

C#とは異なり、コンパイラはフォーマット文字列を分析し、引数の数と型がどうあるべきかを決定します。

これを使って、明示的に指定することなくパラメータの型を制約できます。たとえば、以下のコードでは、コンパイラが引数の型を自動的に推論できます。

let printAString x = printf "%s" x
let printAnInt x = printf "%i" x
// 結果は:
// val printAString : string -> unit // 文字列パラメータを取ります
// val printAnInt : int -> unit // 整数パラメータを取ります

F#には、測定単位を定義し、それらをfloatに関連付ける機能があります。測定単位は次にfloatに型として「付加」され、異なる型の混合を防ぎます。これは、必要な場合に非常に便利な別の機能です。

// いくつかの単位を定義
[<Measure>]
type cm
[<Measure>]
type inches
[<Measure>]
type feet =
// 変換関数を追加
static member toInches(feet : float<feet>) : float<inches> =
feet * 12.0<inches/feet>
// いくつかの値を定義
let meter = 100.0<cm>
let yard = 3.0<feet>
// 異なる単位に変換
let yardInInches = feet.toInches(yard)
// 混ぜて使うことはできません!
yard + meter
// ここで通貨を定義
[<Measure>]
type GBP
[<Measure>]
type USD
let gbp10 = 10.0<GBP>
let usd10 = 10.0<USD>
gbp10 + gbp10 // 許可:同じ通貨
gbp10 + usd10 // 許可されない:異なる通貨
gbp10 + 1.0 // 許可されない:通貨を指定していない
gbp10 + 1.0<_> // ワイルドカードを使用して許可

最後の例をみてみましょう。C#では、どのクラスも他のどのクラスとも等価性を比較できます(デフォルトで参照等価性を使用)。一般的に、これは良くないアイデアです!たとえば、文字列と人を比較できるべきではありません。

以下は完全に有効で、問題なくコンパイルされるC#コードです:

using System;
var obj = new Object();
var ex = new Exception();
var b = (obj == ex);

同じコードをF#で書くと、コンパイル時エラーが発生します:

open System
let obj = new Object()
let ex = new Exception()
let b = (obj = ex)

2つの異なる型の等価性をテストしているなら、おそらく何か間違ったことをしているのでしょう。

F#では、型の比較を完全に禁止することさえできます!これは思うほど馬鹿げたことではありません。一部の型では、有用なデフォルトがない場合や、オブジェクト全体ではなく特定のフィールドに基づいて等価性を強制したい場合があるかもしれません。

以下はその例です:

// 比較を拒否
[<NoEquality; NoComparison>]
type CustomerAccount = {CustomerAccountId: int}
let x = {CustomerAccountId = 1}
x = x // エラー!
x.CustomerAccountId = x.CustomerAccountId // エラーなし