OCaml:如何以Pythonic方式构造格式化字符串?
这一切都源于一个简单的想法:如何在OCaml中写出类似Python的格式化字符串。
在Python中,你可以这样初始化一个字符串:
str = "this var: %s" % this_var
str2 = "this: %s; that: %s" % (this_var, that_var)
而在OCaml中,格式化字符串的代码是:
let str = Printf.sprintf "this var: %s" this_var
let str2 = Printf.sprintf "this: %s; that: %s" this_var that_var
我相信我可以做一些事情,让OCaml的字符串格式化代码看起来像Python的那样。最开始,我定义了一个函数,如下所示:
let (%) s x = Printf.sprintf s x
这样,我就可以直接写:
let str = "this: %s" % "sth"
但是这个简单的函数无法处理更复杂的情况,比如两个或更多的变量。所以我想写一个稍微复杂一点的函数,完美模拟Python的方式。我写成了下面这样:
let (%) s li =
let split_list = Str.full_split (regexp "%[a-z]") s in
let rec fmt result_str s_list x_list = match s_list with
| [] -> result_str
| shd::stl -> match shd with
| Text t -> fmt (result_str^t) stl x_list
| Delim d -> match x_list with
| [] -> fmt result_str stl []
| xhd::xtl -> fmt (result_str^(Printf.sprintf d xhd)) stl xtl
in
fmt "" split_list li
但是这个函数就是不能工作,因为出现了类型错误,而且OCaml的列表不能包含多种类型。如果你写类似这样的代码:"name: %s; age: %d" % ["John"; 20]
,OCaml编译器会嘲笑这段代码,并告诉你一些类型错误。
显然,我必须用元组来替代列表。但我就是不知道如何对一个可变长度的元组进行尾递归。
欢迎任何建议。其实我有两个问题。
- 如何写出一个Python风格的OCaml代码来格式化字符串。
如果OCaml不能动态生成某些字符串作为格式化字符串并传递给sprintf?例如这段代码:
let s = "%s" in Printf.sprintf s "hello"
会产生如下错误信息:
错误:这个表达式的类型是字符串,但期望的表达式类型是 ('a -> 'b, unit, string) 格式 = ('a -> 'b, unit, string, string, string, string) 格式6
5 个回答
为什么有人想用动态格式化来替代静态检查的sprintf呢?OCaml的Printf使用起来简单又安全,而C语言的printf虽然简单,但不安全;C++的流式输出虽然安全,但写起来比较啰嗦。至于Python的格式化,跟C的printf比起来也没什么优势(除了出错时会抛出异常,而不是直接崩溃)。
唯一能想到的情况就是格式字符串来自外部源。这种情况下,通常最好还是在编译时就确定格式。如果实在做不到,那就只能手动进行动态格式化,并处理可能出现的错误(正如之前提到的,不能用Printf处理动态格式字符串)。顺便提一下,像国际化这种情况,可以用现有的库来解决。一般来说,如果想动态组合多个不同类型的值,就得把它们用变体包裹起来(比如['S "hello"; 'I 20]
),然后在打印的时候进行模式匹配。
(1) 我觉得没有比直接使用 Printf.sprintf
更好的方法了。我的意思是,你可以在你已经想到的基础上进行扩展:
let (%) = Printf.sprintf
let str3 = ("this: %s; that: %s; other: %s" % this_var) that_var other_var
这个方法是可行的,但因为需要加括号来确保优先级,所以看起来有点丑。
(2) 在运行时生成格式字符串真的很难,因为格式字符串是在编译时解析的。它们看起来像普通字符串,但实际上是另一种类型,叫做“某种格式6”。(它会根据推断的类型来判断你想要的是字符串还是格式字符串)实际上,格式字符串的确切类型取决于格式中有哪些占位符;这也是它能够检查格式参数的数量和类型的唯一方法。最好不要随便搞格式字符串,因为它们和类型系统紧密相关。
其实这是可以实现的,如果你的操作符是以#
这个字符开头,因为这个字符的优先级比函数调用要高。
let (#%) = Printf.sprintf;;
val ( #% ) : ('a, unit, string) format -> 'a = <fun>
"Hello %s! Today's number is %d." #% "Pat" 42;;
- : string = "Hello Pat! Today's number is 42."