typed/racket 为什么拒绝递归列表函数中的 cons 调用?
问题出在 cons 的第二个参数。按照当前函数签名,递归调用可能返回字符串或列表,而 cons 在这里需要列表作为尾部。将列表分支改用 map,可以让返回类型明确为列表。
为什么 cons 无法通过类型检查?
你的类型定义是:
(define-type RecurListofString
(U String (Listof RecurListofString)))
这表示 RecurListofString 可以是字符串,也可以是由同类值组成的列表。
报错对应的代码是:
(cons
(string-split* (car s) lsep)
(string-split* (cdr s) lsep))
两个递归调用的返回类型都由函数签名决定,都是 RecurListofString。因此,类型检查器看到的参数类型与日志中显示的一样:
Arguments: RecurListofString RecurListofString
第一个参数可以是任意一个递归元素。第二个参数则需要是 (Listof RecurListofString),这样构造出的列表才符合递归类型定义。
即使进入列表分支后 (cdr s) 是列表,函数签名仍只保证返回 RecurListofString,没有说明列表输入一定对应列表输出。因此,类型检查器无法将该递归调用的结果确定为列表。
错误信息中还有一种 cons 类型:
a b -> (Pairof a b)
它可以构造一般的 pair,但如果第二个参数是字符串,结果就可能是不正规列表,例如:
(cons "a" "b") ; '("a" . "b")
这种值不属于你定义的 RecurListofString。
使用 map 处理列表分支
列表分支需要逐个处理元素并保留外层列表结构,可以直接使用 map:
#lang typed/racket
(define-type RecurListofString
(U String (Listof RecurListofString)))
(: string-split*
(-> RecurListofString
(Listof String)
RecurListofString))
(define (string-split* s lsep)
(cond
[(null? s) '()]
[(null? lsep) s]
[(string? s)
(string-split*
(string-split s (car lsep))
(cdr lsep))]
[else
(map
(lambda ([part : RecurListofString])
(string-split* part lsep))
s)]))
代码保留了原有的分支顺序。执行到 else 时,s 已经排除了字符串的情况,因此是列表。
传给 map 的函数接收并返回 RecurListofString,所以 map 的结果类型是:
(Listof RecurListofString)
这个类型属于 RecurListofString 联合类型,符合函数的返回类型要求,也不需要单独证明某次递归调用的结果可以作为列表尾部。
修改后的行为
例如:
(string-split* '("a,b" "c,d") '(","))
;; => '(("a" "b") ("c" "d"))
这里 map 保留外层列表,将每个字符串拆分得到的列表放回对应位置。这与原先用 car、cdr 和 cons 遍历列表的意图一致。
map 不会将嵌套结果展平成单层列表。递归类型允许嵌套列表,原实现也会保留这种结构。如果希望最终得到扁平的字符串列表,就需要另行定义展平规则。
如果仍希望保留 cons
也可以增加一个专门处理列表的辅助函数,明确声明其返回类型为 (Listof RecurListofString)。这样,辅助函数对列表尾部的递归调用就有足够精确的类型,可以作为 cons 的第二个参数。
不要将现有 string-split* 的返回类型直接改成列表。lsep 为空时,原代码会直接返回 s,而 s 可能是字符串;这样修改会与现有行为冲突。
备注:内容仅供参考。
