🦊

Gleamのopaque(不透明型)について

2024/04/06に公開

今回は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)

Playground

偶数を想定した型に奇数が入っているとバグの原因になってしまいそうです。
どうにか値が正しいかどうか作成時に判断できると良さそうです。

opaqueを使ってみる

その前にGleamにおけるモジュールの可視性について簡単に説明します。

Gleamではファイル単位でモジュールを作ることができます。
モジュールで定義した型や関数は定義時にpubキーワードで外部からアクセスすることを許可できます。

mod.gleam
pub type PubType {
  PublicType
}

type NonPubType {
  NonPublicType
}
main.gleam
import gleam/io
import mod

pub fn main() {
  let p = mod.PublicType
  let np = mod.NonPubType // mainからはアクセスできない
}

opaqueでIntに制約を付ける

以下はopaqueを使い、Evenという偶数を定義したモジュールからその型を使う例です。
失敗する可能性がある型の生成結果はResultを用いることで表現できます。

main.gleam
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 // 直接アクセスできない
}
even.gleam
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であると定義しています。

https://x.com/Comamoca_/status/1775907834276020266

もっと実用的な例として、メールアドレスのバリデーションなどが挙げられます。

mail_address.gleam
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.")
  }
}
main.gleam
import gleam/io
import mail_address

pub fn main() {
  // 文字列がメールアドレスであると型レベルで証明できる
  let assert Ok(address) = mail_address.from_string("gleam-sample@example.com")

  address
  |> io.debug
}
GitHubで編集を提案

Discussion