0

レコードタイプを構築しているとしましょう:

type thing {
  fruit: string;
}

fruitしかし、可能な値を文字列の固定セットに制限したいと考えています。

これを OCaml でバリアントとしてモデル化するのは自然なことのようです。

type fruit = APPLE | BANANA | CHERRY

type thing {
  fruit: fruit;
}

ここまでは大丈夫です。

しかし、[@@deriving yojson]これらの型で使用すると、シリアル化された出力は次のようになります。

{ "fruit": ["APPLE"] }

デフォルトでは、Yojson はバリアントをタプルとしてシリアライズしたいと考えて[<name>, <args>...]います...そのロジックはわかりますが、ここでは役に立ちません。

次のようにシリアライズしたい:

{ "fruit": "APPLE" }

いくつかの ppx 派生プラグインを使用して、このモジュールをビルドして、必要に応じてデ/シリアル化することができました。

module Fruit = struct
  type t = APPLE | BANANA | CHERRY [@@deriving enum, variants]

  let names =
    let pairs i (name, _) = (name, (Option.get (of_enum i))) in
    let valist = List.mapi pairs Variants.descriptions in
    List.to_seq valist |> Hashtbl.of_seq
  
  let to_yojson v = `String (Variants.to_name v)

  let of_yojson = function
    | `String s -> Hashtbl.find_opt names s
                   |> Option.to_result ~none:(Printf.sprintf "Invalid value: %s" s)
    | yj -> Error (Printf.sprintf "Invalid value: %s" (Yojson.Safe.to_string yj))
end

これは問題なく動作します...しかし、同じ方法で扱いたい他の「文字列列挙型」バリアントがいくつかあります。毎回このコードをコピーして貼り付けたくありません。

私はこれまでに得ました:

module StrEnum (
  V : sig
    type t
    val of_enum : int -> t option
    module Variants : sig
      val descriptions : (string * int) list
      val to_name : t -> string
    end
  end
) = struct  
  type t = V.t

  let names =
    let pairs i (name, _) = (name, (Option.get (V.of_enum i))) in
    let valist = List.mapi pairs V.Variants.descriptions in
    List.to_seq valist |> Hashtbl.of_seq
  
  let to_yojson v = `String (V.Variants.to_name v)

  let of_yojson = function
    | `String s -> Hashtbl.find_opt names s
                  |> Option.to_result ~none:(Printf.sprintf "Invalid StrEnum value: %s" s)
    | yj -> Error (Printf.sprintf "Invalid StrEnum value: %s" (Yojson.Safe.to_string yj))
end

module Fruit = struct
  type t = APPLE | BANANA | CHERRY [@@deriving enum, variants]
end

module FruitEnum = StrEnum (Fruit)

それは型チェックのようで、次のことができます。

utop # Yojson.Safe.to_string (FruitEnum.to_yojson Fruit.APPLE);;
- : string = "\"APPLE\""

utop # FruitEnum.of_yojson (Yojson.Safe.from_string "\"BANANA\"");;
- : (FruitEnum.t, string) result = Ok Fruit.BANANA

...しかし、私がしようとすると:

type thing {
  fruit: FruitEnum.t;
}
[@@deriving yojson]

私は得るError: Unbound value FruitEnum.t

バリアントのモジュールから再エクスポートしているためのようですがtype t = V.t、よくわかりません。(または、yojson ppx がファンクターの結果を正しく「見る」ことができないためですか?)
どうすればこれを修正できますか?

また、バリアント モジュールを個別に定義するのをスキップして、次のようにできるようにしたいと考えています。

module Fruit = StrEnum (struct
  type t = APPLE | BANANA | CHERRY [@@deriving enum, variants]
end)

...しかし、これによりエラーが発生します:

Error: This functor has type
       functor
         (V : sig
                type t
                val of_enum : int -> t option
                module Variants :
                  sig
                    val descriptions : (string * int) list
                    val to_name : t -> string
                  end
              end)
         ->
         sig
           type t = V.t
           val names : (string, t) Hashtbl.t
           val to_yojson : t -> [> `String of string ]
           val of_yojson : Yojson.Safe.t -> (t, string) result
         end
       The parameter cannot be eliminated in the result type.
       Please bind the argument to a module identifier.

何が悪いのかわかりません。

4

2 に答える 2

2

最後のエラーに関しては、OCaml がモジュール内の型を参照できるように「安定したパス」を必要とするためです。安定したパスは、タイプへの名前付きパスFruit.tです。

対照的に、型は名前を持たないモジュール リテラルの型を参照しているStrEnum(struct type t = ... end).tため、安定したパスではありません。tt

簡単に言えば、基本的にバリアント モジュールを個別に定義することをスキップすることはできません。ただし、次の 2 つの手順で簡単に実行できます。

module Fruit = struct
  type t = ...
end

module Fruit = StrEnum(Fruit)

2 番目の定義は、最初の定義を参照し、それを隠します。シャドウイングは、OCaml でよく知られており、頻繁に使用される手法です。

全体として、この PPX 機構のすべてが実際に正当化されているかどうかはわかりません。コンバーター関数は非常に簡単に手書きできます。

let to_yojson = function
  | APPLE -> `String "APPLE"
  | BANANA -> `String "BANANA"
  | CHERRY -> `String "CHERRY"
于 2021-06-06T18:50:16.807 に答える