如何定义 return 类型取决于其参数值的函数?

How can you define a function who's return type depends on the value of its parameter?

假设我想定义一个名为 zero 的函数,它采用 "f""i" 之类的字符串和 returns float 或 int 的零值。

所以我们可以做这样的事情(我没有真正的实现,这只是模拟我想要的):

utop # zero "f"
- : float = 0.0
utop # zero "i"
- : int = 0

OCaml 中的哪些语言特性使这类事情成为可能?您如何实际使用这些功能来完成类似的事情? (即有人可以 show/explain 怎么做吗?)。

PS:在几乎任何其他语言中,我都不认为这样的事情是可能的。但我认为在 OCaml 中是!我这么认为是因为 Stdlib 中的 Printf.printf 做了一些非常相似的事情(本质上相似,但在细节上要复杂得多)。 IE。这些示例的共同点是,Printf.printf "..." 也有一个静态类型 depends/varies 基于作为其第一个参数传递的值。

我试图通过查看 Printf 的源代码来理解它是如何工作的,但它有点太复杂,太深了,我无法理解。我希望上面 zero 函数的这个更简单但相似的例子能让某人解释解决方案的本质,而不需要 printf 的 'formatting syntax'.

的所有复杂性

具有 return 类型的函数依赖于值需要依赖类型系统。这不是OCaml核心语言的情况(模块语言是依赖类型的)。​​

Printf 的情况是 Printf 的第一个参数不是 string 而是 format string。编译器中有一点魔法可以使 format strings 在语法中显示为字符串,但 format strings 实际上是广义代数数据类型 (GADT)。

如果我们知道在哪里看,您可以在顶层观察它。例如,

open CamlinternalFormatBasics
let s: _ format6 = "%s";;

将打印

val s : (string -> 'a, 'b, 'c, 'd, 'd, 'a) format6 =
  Format (String (No_padding, End_of_format), "%s")

换句话说,"%s"Format (String (No_padding, End_of_format), "%s")的语法糖。

在实现方面,看一个只有string个空洞和文字的格式字符串的简化版本会更简单一些:

type 'args fmt =
  | End: unit fmt
  | String_hole: 'inner_args fmt -> (string -> 'inner_args) fmt
  | Lit: string * 'args fmt -> 'args fmt
let s = Lit("Hello ", String_hole End)

此处,值 s 是“Hello %s”的简化等效项,类型为 (string -> unit) fmt。同样,

let s2 = String_hole (String_hole End)

(又名 "%s%s")是类型 (string->string->unit) fmt 的值。换句话说,类型 'a fmt 的值的类型 'a 跟踪需要多少个字符串来填充格式字符串中的所有字符串孔。

由于信息存在于类型中,我们可以将 printf 的类型描述为:'a fmt -> 'a(没有任何依赖类型)。函数 printf return 是一个函数,它需要与 returning unit 之前格式字符串中的空洞一样多的参数。事实上,可以写下这样的 printf 函数:

let rec printf: type a.  a fmt -> a = fun fmt ->
  match fmt with
  | End -> print_newline ()
  | Lit (lit,inner_fmt) -> print_string s; printf inner_fmt
  | String_hole inner_fmt -> fun s -> print_string s; printf inner_fmt

let () = printf s "world"
let () = printf s2 "Hello" "universe" 

之所以可行,是因为 GADT 允许在变体构造函数和代数数据类型的结果类型之间表达更精细的关系。

例如,如果我在模式匹配的分支中观察构造函数 End,我知道类型 a 实际上是 unit,并且我可以 return什么都不做。

同样,如果我看到 Lit(lit,inner_fmt),我知道 inner_fmt 与其父项具有相同的类型 a fmt。换句话说,它需要与其父级一样多的参数,我可以打印字符串 lit 并继续评估我的格式字符串。

真正有趣的例子是字符串孔 String_hole fmt,它告诉我我的类型是 (string -> $inner_args) fmt。换句话说,父格式字符串比其子格式字符串需要多一个参数。因此,我必须 returns 一个接受字符串参数的函数,在恢复内部格式字符串的评估之前打印它的参数。

OCaml 实现从这个想法开始,并在格式字符串中添加了对许多孔类型的支持。

因此 OCaml 中实现的 printf 不需要依赖类型,只需要 GADT。

虽然来自 octachrons 答案的格式类型解释了为什么 printf 可以有一个 return 类型,具体取决于格式字符串的值,但我认为这不是问题的正确答案。主要是因为格式字符串不仅限于一个值。应该 zero "%d%s" return?

所以让我给出一个使用 GADT 的替代方案:

# type 'a t =
| Int : int t
| Float : float t;;
type 'a t = Int : int t | Float : float t

类型 'a t 是 GADT(广义代数数据类型),其中每个变体都有不同的类型。函数,如零,然后可以具有 return 类型,具体取决于 GADT 的类型。

您可能只想写这个:

# let zero = function
  | Int -> 0
  | Float -> 0.0;;
Error: This pattern matches values of type float t
       but a pattern was expected which matches values of type int t
       Type float is not compatible with type int 

不幸的是,Ocamls 类型推断不会自动理解 GADT,需要一点帮助。您必须始终为类型系统注释 GADT 以了解正在发生的事情。必须像这样指定 GADT 类型:

# let zero : type a . a t -> a = function
| Int -> 0
| Float -> 0.0;;
val zero : 'a t -> 'a = <fun>
# zero Int;;
- : int = 0
# zero Float;;
- : float = 0.