Closed14

Variant 型 | OCaml

d3d3

作り方に何種類か方法があるようなデータ

Variant型の例:図形を表す

図形の種類と、後で面積を計算したいので、そのための付随したデータを持っているデータ型

  • 点 - 形、大きさはない
  • 円 - 半径がわかれば面積を計算できる
  • 長方形 - 長辺と短辺の長さで面積がわかる
  • 正方形 - 一辺の長さで面積がわかる
type figure =
 | Point
 | Circle of int
 | Rectangle of int * int
 | Square of int

Variant型の例:色を表す

(* ターミナルの基本色 *)
type basic_color =
  | Black
  | Red
  | Green
  | Yellow
  | Blue
  | Magenta
  | Cyan
  | White
d3d3

端末の色を整数コードにマッピングする:

(* 基本色を整数コードに変換する *)
let basic_color_to_int = function
  | Black -> 0
  | Red -> 1
  | Green -> 2
  | Yellow -> 3
  | Blue -> 4
  | Magenta -> 5
  | Cyan -> 6
  | White -> 7

let _ = basic_color_to_int Green |> printf "color: %d\n"
color: 2
d3d3

これを使って文字色を変更するためのエスケープした表現を得られる:

let color_by_number number text =
  Printf.sprintf "\027[38;5;%dm%s\027[0m" number text

let red_text = color_by_number (basic_color_to_int Red) "Hello Red Text!"
let _ = print_endline red_text

let blue_text = color_by_number (basic_color_to_int Blue) "Blue"
let _ = printf "Hello %s Text!" blue_text

utopで試してみる:


d3d3
let c = Red

は単にRedというタグを名前に束縛しているだけなので、enumと同じようなものだが
Variant型ではタグに対応するデータを組み合わせることができる。

d3d3

多くの端末は、256の異なる色をサポートし、次のグループに分かれています。

  • 8つの基本色とウェイト(regular/bold)
  • 6×6×6の RGB
  • 24レベルのグレースケール

より複雑な色のグループを表す:

type weight = Regular | Bold

type color =
  | Basic of basic_color * weight
  | RGB of int * int * int
  | Gray of int

これで、3種類の色を表すデータを定義することができるようになる:

RGB(250, 70, 70)

Basic (Green, Regular)

Gray (123)
d3d3
(* 色を対応する数値に変換する *)
let color_to_int = function
  | Basic (basic_color, weight) ->
      let base = match weight with
      | Bold -> 8
      | Regular -> 0
      in
      base + basic_color_to_int basic_color
  | RGB (r, g, b) -> 16 + b + (g * 6) + (r * 36)
  | Gray i -> 232 + i

(* 色情報を加えて出力する *)
let color_print color s = printf "%s\n" (color_by_number (color_to_int color) s)

d3d3

複数の引数を持つVariantはタプルのように見えるが、タプルではないことに注意:

type color =
  | Basic of basic_color * weight
  | RGB of int * int * int
  | Gray of int
# RGB(255, 255, 255);;
- : color = RGB (255, 255, 255)

# let black_t = (255, 255, 255);;
val black_t : int/12 * int/12 * int/12 = (255, 255, 255)

# RGB black_t;;
Error: The constructor RGB expects 3 argument(s),
       but is applied here to 1 argument(s)

タプルを含むVariantにしたい場合:

type tupled = Tupled of (int * int);;
d3d3

型システムをリファクタリングツールとして使う

インターフェイスの変更に合わせて更新する必要がある場所を警告してくれる。

color の定義を変えてみる:

(*
type color =
  | Basic of basic_color * weight
  | RGB of int * int * int
  | Gray of int
*)

type color =
  | Basic of basic_color
  | Bold of basic_color
  | RGB of int * int * int
  | Gray of int

以下のように、color_to_intはエラーになる。

let color_to_int = function
  | Basic (basic_color, weight) ->
      let base = match weight with Bold -> 8 | Regular -> 0 in
      base + basic_color_to_int basic_color
  | RGB (r, g, b) -> 16 + b + (g * 6) + (r * 36)
  | Gray i -> 232 + i

This pattern matches values of type 'a * 'b
but a pattern was expected which matches values of type basic_color

以下のように Basic / Bold に対応するように変更:

let color_to_int = function
  | Basic basic_color -> basic_color_to_int basic_color
  | Bold basic_color -> 8 + basic_color_to_int basic_color
  | RGB (r, g, b) -> 16 + b + (g * 6) + (r * 36)
  | Gray i -> 232 + i
d3d3

タイプエラーのおかげでリファクタリングを完了するために必要な修正箇所を特定できるが、このためにパターンマッチでの「Catch-all Case」を避けた方が良い。

d3d3

Record と Variant の組み合わせ

  • Variant: 複数の異なる形式のデータを一つの型として表現できる(直和)
  • タプルやレコード: 複数の異なる型のデータを組み合わせて一つの型として表現できる(直積)

代数的データ型は直和と直積の階層的な組み合わせで複雑なデータを構築することができる。

d3d3
  • Record: 結合・関連
  • Variants: 分離・複数の可能性

一つのログメッセージを表す Log_entry とそれに付随する他のメッセージの例:

module Time_ns = Core.Time_ns

module Log_entry = struct
  type t = {
    session_id : string;
    time : Time_ns.t;
    important : bool;
    message : string;
  }
end
module Heartbeat = struct
  type t = {
    session_id : string;
    time : Time_ns.t;
    status_message : string
  }
end

module Logon = struct
  type t = {
    session_id : string;
    time : Time_ns.t;
    user : string;
    credentials : string;
  }
end
d3d3

Variant はこれら3つ型のいずれかである可能性がある値を表現する:

type client_message =
  | Logon of Logon.t
  | Heartbeat of Heartbeat.t
  | Log_entry of Log_entry.t

固定のタイプのメッセージに特化したコードではなく、メッセージを一般的に処理したいコードを書きたい場合は
複数のメッセージを包括的に表した↑のような型が必要になる。

messagesを走査し

  1. userと一致するLogonメッセージを見つけたら
  2. そのsession_idを記録し
  3. その後のHeartbeatやLog_entryメッセージで同じsession_idがあれば
  4. そのメッセージを結果に追加
  5. 最後に、List.revを使ってメッセージの順序を反転
open Base

(* client_message のリストを受け取り
   特定のユーザによって生成されたメッセージを返す *)
(* string -> client_message list -> client_message list *)
let message_for_user user messages =
  let user_messages, _ =
    List.fold messages
      ~init:([], Set.empty (module String))
      ~f:(fun ((messages, user_sessions) as acc) message ->
        match message with
        | Logon msg ->
            if String.(msg.user = user) then
              (message :: messages, Set.add user_sessions msg.session_id)
            else acc
        | Heartbeat _ | Log_entry _ ->
            let session_id =
              match message with
              | Logon msg -> msg.session_id
              | Heartbeat msg -> msg.session_id
              | Log_entry msg -> msg.session_id
            in
            if Set.mem user_sessions session_id then
              (message :: messages, user_sessions)
            else acc)
  in
  List.rev user_messages
d3d3

このコードだと各ケースでsession_idを抽出する部分が反復的なので、メッセージの型を変えてみる:

module Log_entry = struct
  type t = { important : bool; message : string }
end

module Heartbeat = struct
  type t = { status_message : string }
end

module Logon = struct
  type t = { user : string; credentials : string }
end

module Common = struct
  (* メッセージ共通のフィールド *)
  type t = { session_id : string; time : Time_ns.t }
end

type details =
  | Logon of Logon.t
  | Heartbeat of Heartbeat.t
  | Log_entry of Log_entry.t
d3d3

各メッセージは (Common.t * details)で表すことができる:

(* メッセージのリストを受け取り
   特定のユーザによって生成されたメッセージを返す *)
(* string -> (Common.t * details) list -> (Common.t * details) list *)
let message_for_user user (messages : (Common.t * details) list) =
  let user_messages, _ =
    List.fold messages
      ~init:([], Set.empty (module String))
      ~f:(fun
          ((messages, user_sessions) as acc) ((common, details) as message) ->
        match details with
        | Logon d ->
            if String.( = ) d.user user then
              (message :: messages, Set.add user_sessions common.session_id)
            else acc
        | Heartbeat _ | Log_entry _ ->
            if Set.mem user_sessions common.session_id then
              (message :: messages, user_sessions)
            else acc)
  in
  List.rev user_messages
このスクラップは2024/02/07にクローズされました