Variant 型 | OCaml
作り方に何種類か方法があるようなデータ
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
端末の色を整数コードにマッピングする:
(* 基本色を整数コードに変換する *)
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
これを使って文字色を変更するためのエスケープした表現を得られる:
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で試してみる:
let c = Red
は単にRed
というタグを名前に束縛しているだけなので、enumと同じようなものだが
Variant型ではタグに対応するデータを組み合わせることができる。
多くの端末は、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)
(* 色を対応する数値に変換する *)
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)
複数の引数を持つ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);;
型システムをリファクタリングツールとして使う
インターフェイスの変更に合わせて更新する必要がある場所を警告してくれる。
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
タイプエラーのおかげでリファクタリングを完了するために必要な修正箇所を特定できるが、このためにパターンマッチでの「Catch-all Case」を避けた方が良い。
Record と Variant の組み合わせ
- Variant: 複数の異なる形式のデータを一つの型として表現できる(直和)
- タプルやレコード: 複数の異なる型のデータを組み合わせて一つの型として表現できる(直積)
代数的データ型は直和と直積の階層的な組み合わせで複雑なデータを構築することができる。
- 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
Variant はこれら3つ型のいずれかである可能性がある値を表現する:
type client_message =
| Logon of Logon.t
| Heartbeat of Heartbeat.t
| Log_entry of Log_entry.t
固定のタイプのメッセージに特化したコードではなく、メッセージを一般的に処理したいコードを書きたい場合は
複数のメッセージを包括的に表した↑のような型が必要になる。
messages
を走査し
- userと一致するLogonメッセージを見つけたら
- そのsession_idを記録し
- その後のHeartbeatやLog_entryメッセージで同じsession_idがあれば
- そのメッセージを結果に追加
- 最後に、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
このコードだと各ケースで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
各メッセージは (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