不透明型は、Elmにおけるカプセル化の手法です。情報隠蔽、あるいは実装の詳細の隠蔽と呼ぶ人もいます。また、Parse, dont validateパターンを使って検証を強制するのにもよく使われます。たとえば、有効なメールアドレスしか含むことのできないEmailAddressという不透明型を作ることができます(何でも含むことのできるStringとは対照的です)。
カスタム型には1つ以上のバリアントがあり、コンパイラーはバリアント名を使って、各バリアントを作成する関数を自動的に生成します。カスタム型を作成できるのは、これが唯一の方法です。コンパイラーは、これらのバリアントに対する分解も可能にします。
type Maybe a
= Nothing --> Variant implies a 'Nothing' function, with type 'Maybe a', and destructuring of 'Nothing'
| Just a --> Variant implies a 'Just' function, with type 'a -> Maybe a', and destructuring of 'Just _'
デフォルトでは、これらはすべてファイル/モジュール内部に閉じています。何をエクスポートするかは、exposingキーワードで選びます。不透明型を作るには、バリアントをエクスポートしないように選ぶだけです。
そうすると、ほとんどの場合、カスタム関数を公開することで、バリアントを作成したり取得したりする別の手段を用意する必要があります。これらのカスタム関数を使えば、バリアントを作成・取得するときに何らかのロジックを適用できます。これによりモジュールの内部実装の詳細が隠され、公開している関数が同じままであれば、実装の詳細をいくらでも変更できるようになります。
おそらく不透明型の最も一般的な用途は、Parse, dont validateパターンを使った検証の強制です。このパターンでは、Stringのように、望むよりも構造や型安全性の低い入力を受け取る関数を公開し、それをEmailAddressのようなより構造化された型へと解析します。戻り値は、その型か、エラー(通常はMaybeやResult)です。
たとえば、以下のコードでは、外部のモジュールがEmailAddress型を作成できるのはparseEmailAddress関数を使う場合だけなので、EmailAddress型を扱うときには、それが有効であると確信できます。
module EmailAddress exposing ( EmailAddress, parseEmailAddress, getEmailAddress )
-- The `EmailAddress` custom type has one variant, also called `EmailAddress`
-- When a custom type has one variant, it is idiomatic to use the same name for both.
type EmailAddress =
EmailAddress String
-- create an EmailAddress variant
parseEmailAddress: String -> Maybe EmailAddress
parseEmailAddress candidateEmailAddress =
if isValidEmailAddress candidateEmailAddress then
Just (EmailAddress candidateEmailAddress)
else
Nothing
-- get an EmailAddress variant
getEmailAddress: EmailAddress -> String
getEmailAddress ( EmailAddress emailAdress ) =
emailAdress
isValidEmailAddress: String -> bool
-- ...
この演習では、宝物を保管するための汎用的な(というより魔法のような!)SecureTreasureChestを書いていきます。
勘の鋭い読者なら、以前のTreasureChestがあまり安全ではなかったことにお気づきでしょう!
getTreasure関数を使う場合はパスワードを渡す必要がありましたが、それでもTreasureChest型を分解するだけで、パスワードを知らなくても宝物を取り出せてしまいました。
さらに悪いことに、同じ方法でパスワードまで取り出せてしまいました!
これを、Opaque型を使って解決しましょう。
1つのバリアントを持つPasswordカスタム型を定義します。
そのバリアントには、パスワードを表すString値を関連付けます。
Password型は公開しますが、バリアントは公開しません。
これによりPasswordはOpaque型になり、分解を使ってパスワードを取り出すことはできなくなります。
型がOpaqueかどうかをテストで確かめることはできないので、実際のコードでは、それを強制するためにelm-review(静的解析ツール)を使います。
Stringを受け取ってMaybe Passwordを返すcreatePassword関数を、型定義とともに定義して公開します。
OWASPはパスワードは8文字以上の長さにすることを推奨しています。そこで、この不変条件を強制するために、Parse, don't validateパターンを使いましょう。
パスワードが8文字未満の場合、createPasswordはNothingを返し、それ以外の場合はJust Passwordを返します。
SecureTreasureChest型は公開しますが、バリアントは公開しません。
これによりSecureTreasureChestはOpaque型になり、分解を使ってパスワードや宝物を取り出すことはできなくなります。
この関数は2つの引数を受け取り、型定義を持ちます。
Password(パスワード用)この関数はSecureTreasureChestを返します。
他のモジュールがSecureTreasureChestのインスタンスを作成できるように、この関数を公開します。
この関数は2つの引数を受け取り、型定義を持ちます。
String(パスワード用)SecureTreasureChestカスタム型この関数は、渡されたパスワードを、SecureTreasureChest内のPasswordと照合します。
関数はMaybeを返します。
パスワードが一致すれば、SecureTreasureChestから取り出した汎用的な値(宝物です!)を持つJustを返します。
パスワードが一致しなければNothingを返します。
正しいパスワードを渡しさえすれば、他のモジュールがSecureTreasureChestから宝物を取り出せるように、この関数を公開します。