TL;DR

  • 同じ int でも CustomerId と OrderId は別物なので、プリミティブ値を単純型で包んで区別する。
  • 単純型にはメモリと速度のコストがあるので、必要なら型エイリアスや構造体で代替する。

内容

単一ケースの判別共用体

ProductCode や OrderId は、どちらも int で表せても互換性はない。型が違うことを明確にするため、プリミティブな値をラップする。F# では、ケースが 1 つだけの判別共用体(単純型)を使う。

type CustomerId = CustomerId of int

ケース名をコンストラクタ関数として使うと、値を作れる。

let customerId = CustomerId 42

こうすると、異なる型を誤って混同するとコンパイルエラーになる。

type CustomerId = CustomerId of int
type OrderId = OrderId of int
 
let customerId = CustomerId 42
let orderId = OrderId 42
 
printfn "%b" (orderId = customerId) // コンパイルエラー
 
let processCustomerId (id : CustomerId) = ...
processCustomerId orderId // コンパイルエラー

中の値を取り出すときは、ケースラベルでパターンマッチをする。関数のパラメータで直接取り出すこともできる。

let customerId = CustomerId 42
 
let (CustomerId innerValue) = customerId
printfn "%i" innerValue // 42
 
// val processCustomerId: CustomerId -> unit
let processCustomerId (CustomerId innerValue) =
    printfn "innerValue is %i" innerValue

パフォーマンスの問題

単純型は、メモリ使用量と効率を犠牲にする。アプリケーションの種類によっては注意が必要である。代替策は 3 つある。

1 つ目は型エイリアスである。オーバーヘッドはないが、型の安全性は下がる。

type UnitQuantity = int

2 つ目は値型(構造体)である。オーバーヘッドは残るが、配列に入れると連続したメモリ領域に置かれるので、キャッシュの効率がよい。

[<Struct>]
type UnitQuantity = UnitQuantity of int

3 つ目は、単純型のコレクションではなく、プリミティブ値のコレクション全体を 1 つの型として定義する方法である。

type UnitQuantities = UnitQuantities of int[]

ただし一般には、まず最も分かりやすい方法でドメインをモデリングし、その後でチューニングや最適化に取り組むのがよい。

キーワード