映射多态变体的子集

时间:2015-04-12 12:30:54

标签: ocaml

我经常想要将函数应用于某个变量中的值,以便函数的结果与输入具有相同的类型。如何使类型成功?这是我目前的尝试:

module T : sig
  type generic =
    [ `Foo of int
    | `Bar of int ]

  val map : (int -> int) -> ([< generic] as 'a) -> 'a

(*   val a : [`Foo of int] *)
end = struct
  type generic =
    [ `Foo of int
    | `Bar of int ]

  let map fn = function
    | `Foo x -> `Foo (fn x)
    | `Bar x -> `Bar (fn x)
  let map : (int -> int) -> ([< generic] as 'a) -> 'a = Obj.magic map

(*
  let a : [`Foo of int] = `Foo 1 |> map succ
  let b : [`Bar of int] = `Bar 1 |> map succ
*)
end

这是按原样运行的,但是如果我取消注释let a行,那么我得到:

   Values do not match:
     val map : (int -> int) -> [ `Foo of int ] -> [ `Foo of int ]
   is not included in
     val map : (int -> int) -> ([< generic ] as 'a) -> 'a

似乎定义a已更改map的类型,这似乎很奇怪。

另一方面,在模块结束后放置它:

open T
let a : [`Foo of int] = `Foo 1 |> map succ
let b : [`Bar of int] = `Bar 1 |> map succ

最后,如果我将map的定义更改为:

  let map : 'a. (int -> int) -> ([< generic] as 'a) -> 'a = Obj.magic map

(即只是在开头添加一个明确的'a.)然后抱怨:

Error: This definition has type (int -> int) -> ([< generic ] as 'a) -> 'a
   which is less general than
     'b. (int -> int) -> ([< generic ] as 'b) -> 'b

有人可以解释一下发生了什么吗?有一个更好的方法吗?我可以使用GADT来避免使用Obj.magic,但是我必须将它传递给我希望避免的每个函数调用。

关于真实系统的注意事项

在我的实际程序中,我有各种节点类型(AreaProjectActionContact等等,并且不同的操作适用于不同的类型,但有些是常见的。

例如,with_name可以重命名任何节点类型,但如果我重命名Action,则结果必须是另一个操作。如果我重命名[Area | Project | Action],那么结果必须是[Area | Project | Action]等等。

我最初使用了一个元组,其中包含常见的详细信息(例如(name * Action ...)),但这使得用户很难匹配不同类型(特别是因为它们是抽象的)并且某些功能很常见到子集(例如,只有ProjectAction可以加星标。)

3 个答案:

答案 0 :(得分:3)

你只是被价值限制咬了一口。类型检查器不知道Obj.magic map中的类型,特别是它们的注入/方差,不能立即推广并等待模块边界。如果您使用merlin,它会显示以下类型:(int -> int) -> (_[< generic ] as 'a) -> 'a。注意显示单态类型变量的_。类型变量在第一次使用时专门化,因此当您取消注释a时的行为。

显然,类型检查员设法在模块边界处进行概括,我不会试图猜测为什么会有(Obj。)魔法。

没有明显的解决方案,因为当你输入一个分支时,ocaml不会让你操纵行变量并且不会对类型变量进行专门化,除非使用gadt(这可能值得一个)功能请求。它与this)相关。

如果您只有少数情况,或者没有复杂的组合,您可以尝试:

module T : sig
  type foo = FOO
  type bar = BAR

  type _ generic =
    | Foo : int -> foo generic
    | Bar : int -> bar generic

  val map : (int -> int) -> 'a generic -> 'a generic

  val a : foo generic
  val b : bar generic
end = struct

  type foo = FOO
  type bar = BAR

  type _ generic =
    | Foo : int -> foo generic
    | Bar : int -> bar generic

  let map (type a) fn (x : a generic) : a generic = match x with
    | Foo x -> Foo (fn x)
    | Bar x -> Bar (fn x)

  let a = Foo 1 |> map succ
  let b = Bar 1 |> map succ
end

答案 1 :(得分:2)

类型可能更改的唯一方法是当它很弱时,实际上,模块中的map类型为(int -> int) -> (_[< generic ] as 'a) -> 'a,请注意此_。所以,这意味着,在使用地图之前,您需要离开T模块:

let a = Foo 1 |> T.map succ

充当魅力。

关于更通用的方法,不需要任何魔法,我将使用以下内容。我将明确定义一个多态类型。唯一的区别是,map是从'a t'a ta的映射,而b将具有更通用的类型'a t

module T : sig
  type generic =
    [ `Foo of int
    | `Bar of int ]

  type 'a t = 'a constraint 'a = generic
  val a : 'a t
  val b : 'a t
  val map : (int -> int) -> 'a t -> 'a t
end = struct
  type generic =
    [ `Foo of int
    | `Bar of int ]

  type 'a t = 'a constraint 'a = generic

  let map fn = function
    | `Foo x -> `Foo (fn x)
    | `Bar x -> `Bar (fn x)

  let a = `Foo 1 |> map succ
  let b = `Bar 1 |> map succ
end

但你可能会说,这会破坏整个观点,即你希望map保留其参数的类型,这样如果你用Foo调用它,它应该返回Foo。这只是意味着我们应该为我们的类型选择另一个约束,即使用[< generic]而不是更少的[generic]。换句话说,我们说'a对于generic类型的子集是多态的,即它可以实例化为generic子集的任何类型。有了这个,我们甚至可以隐藏我们的'a t并在我们的签名中仅显示generic类型,从而实现最初的目标:

module T : sig
  type generic =
    [ `Foo of int
    | `Bar of int ]

  val map : (int -> int) -> generic -> generic

  val a : [`Foo of int]
  val b : [`Bar of int]
end = struct
  type generic =
    [ `Foo of int
    | `Bar of int ]

  type 'a t = 'a constraint 'a = [< generic]

  let map fn : 'a t -> 'a t = function
    | `Foo x -> `Foo (fn x)
    | `Bar x -> `Bar (fn x)

  let a : [`Foo of int] = `Foo 1 |> map succ
  let b : [`Bar of int] = `Bar 1 |> map succ
end

答案 2 :(得分:1)

为什么要求map函数的签名为

val map : (int -> int) -> ([< generic] as 'a) -> 'a

当你试图限制变体类型的“开放性”时,对我来说没什么意义。在我看来,有两个自然的签名 对于地图功能。第一种可能性是

val map : (int -> int) -> [< generic] -> [> generic]

这样map f可以应用于generic的子类型,并生成一个自然嵌入generic的任何超类型的值。该 第二种可能性是

val map : (int -> int) -> ([> generic] as 'a) -> 'a

其中map f可以应用于generic的任何超类型,但只会处理generic中的变体,并且可能会保留其他值不变。

第一个签名可以实现为

module T : sig
  type generic =
    [ `Foo of int
    | `Bar of int ]

  val map : (int -> int) -> [< generic ] -> [> generic ]
end = struct
  type generic =
    [ `Foo of int
    | `Bar of int ]

  let map fn = function
    | `Foo x -> `Foo (fn x)
    | `Bar x -> `Bar (fn x)
end

第二个签名可以实现为

module T : sig
  type generic =
    [ `Foo of int
    | `Bar of int ]

  val map : (int -> int) -> ([> generic ] as 'a)-> 'a
end = struct
  type generic =
    [ `Foo of int
    | `Bar of int ]

  let map fn = function
    | `Foo x -> `Foo (fn x)
    | `Bar x -> `Bar (fn x)
    | x -> x
end

编写一个范围是封闭变量的函数对我来说没有意义,应该使用经典的sum类型。也许甚至不可能编写这样的功能,专家可以确认。