Gleamのopaque(不透明型)について
今回はGleamのopaque
(以下不透明型)について解説します。
透明型とは、外部から認識はできるものの、呼び出し元からは直接アクセスできない型を表します。
もっと言うと、プリミティブな型に任意の制限を課すことができる型になります。
この機能を使うとメールアドレスなど形式が決っている文字列や数値などを型として定義したりできます。
TSだとテンプレートリテラル型が近そうです。(そこまで詳しくないのでまちがっているかもしれません)
まずは普通に型を定義してみる
GleamはCustom types
という機能でユーザーが独自の型を作成できます。
例えば、偶数を定義した型について考えてみましょう。
まずはopaqueを使わずに書いてみます。
import gleam/io
pub type Even {
Even(n: Int)
}
pub fn main() {
let _ = even()
}
fn even() {
Even(2) |> io.debug // Even(2)
}
これは見た感じ良さそうですが、大きな問題を抱えています。
偶数じゃない値も入れられるのです。
Even(5) |> io.debug // Even(5)
偶数を想定した型に奇数が入っているとバグの原因になってしまいそうです。
どうにか値が正しいかどうか作成時に判断できると良さそうです。
opaqueを使ってみる
その前にGleamにおけるモジュールの可視性について簡単に説明します。
Gleamではファイル単位でモジュールを作ることができます。
モジュールで定義した型や関数は定義時にpub
キーワードで外部からアクセスすることを許可できます。
pub type PubType {
PublicType
}
type NonPubType {
NonPublicType
}
import gleam/io
import mod
pub fn main() {
let p = mod.PublicType
let np = mod.NonPubType // mainからはアクセスできない
}
opaqueでIntに制約を付ける
以下はopaque
を使い、Even
という偶数を定義したモジュールからその型を使う例です。
失敗する可能性がある型の生成結果はResult
を用いることで表現できます。
pub fn main() {
even.from_int(8)
|> io.debug // Ok(Even(8))
even.from_int(5)
|> io.debug // Error(5)
even.Even(2)
|> io.debug // 直接アクセスできない
}
pub opaque type Even {
Even(num: Int)
}
/// 値が偶数でない場合は与えられた数値をErrorで包んで返す
pub fn from_int(n: Int) -> Result(Even, Int) {
case n % 2 {
0 -> Ok(Even(n))
_ -> Error(n)
}
}
pub fn to_int(n: Even) -> Int {
n.num
}
opaqueの使い道
opaqueを使うとプリミティブな型に任意の制限をかけられるので、型レベルプログラミングに近いことが可能になります。
以下はGleamでFizzbuzzをFizz
Buzz
N
という3つの型のList
であると定義しています。
もっと実用的な例として、メールアドレスのバリデーションなどが挙げられます。
import gleam/regex
pub opaque type EmailAddress {
EmailAddress(address: String)
}
// この関数を呼びださないとEmailAddress型を作れない
pub fn from_string(str: String) -> Result(EmailAddress, String) {
let assert Ok(re) =
"^[a-zA-Z0-9_+-]+(.[a-zA-Z0-9_+-]+)*@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\\.)+[a-zA-Z]{2,}$"
|> regex.from_string
case regex.check(re, str) {
True -> Ok(EmailAddress(str))
False -> Error("Cant convert EmailAddress.")
}
}
import gleam/io
import mail_address
pub fn main() {
// 文字列がメールアドレスであると型レベルで証明できる
let assert Ok(address) = mail_address.from_string("gleam-sample@example.com")
address
|> io.debug
}
Discussion