PAYATHON 2026

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 可能是字符串;这样修改会与现有行为冲突。

备注:内容仅供参考。