函数式 (Scala) 编程:在令牌上拆分 List[Char]

Functional (Scala) programming: Split a List[Char] on a token

递归按标记拆分函数的普遍接受实现是什么?

例如:

val s: List[Char] = 'F' :: 'o' :: 'o' :: ' ' :: 'b' :: 'a' :: 'r' :: ' ' :: 'f' :: 'o':: 'o':: ' ' :: 'b' :: 'a' :: 'r' :: Nil
fooSplit(s) // -> List(List('F','o','o'), List('b', 'a', 'r'), List('f', 'o', 'o'), List('b','a','r'))

以下是我在对参数进行模式匹配时提出的基本情况:

def fooSplit(text: List[Char]): List[List[Char]] = text match {
  case x :: ' ' :: xs =>
    // Somehow append old list to fooSplit(xs) <?>
    println(x + "_")
    fooSplit(xs)

  case x :: xs =>
    println(x + "")
    fooSplit(xs)

  case Nil => Nil
}

我按照上述结构进行的大部分尝试几乎总是导致 List('F', List('O', List('O'), ...)...)。如果没有想出一些非常险恶的双重模式匹配,我似乎无法集中精力扩展而不是附加到内部列表。


我还想引用 Scala Cookbook:

Use flatMap in situations where you run map followed by flatten. The specific situation is this:

• You’re using map (or a for/yield expression) to create a new collection from an existing collection.
The resulting collection is a list of lists.
• You call flatten immediately after map (or a for/yield expression)

这似乎是使用 flatMap 的一个非常可能的情况,有人可以对此提供见解吗?

仅仅因为您要返回 List[List[String]] 的实例并不意味着您需要展平它,有时您可能需要这些值。我建议这样做:

def fooSplit(text: List[Char]): List[List[Char]] = {
  @tailrec
  def fooSplitHelper(currentListBeforeToken: List[Char] = List.empty, list: List[Char], acc: List[List[Char]] = List.empty): List[List[Char]] = {
    if (list.isEmpty) {
      currentListBeforeToken.reverse :: acc
    } else {
      list.head match {
        case ' ' => fooSplitHelper(List.empty[Char], list.tail, currentListBeforeToken.reverse :: acc)
        case otherChar => fooSplitHelper(otherChar :: currentListBeforeToken, list.tail, acc)
      }
    }
  }
  fooSplitHelper(list = text).reverse
}

防止此函数变得“险恶”的一个重要实现是,您可以将正在构建的当前子列表保存为它自己的变量,并且仅在完成时将其添加到结果中。

从那里开始,它非常简单。

基本上有三种情况:列表末尾(完成!),当前元素是 space(将中间添加到结果,并再次从空开始),其他一切(继续添加到中间),所以你只需拼写出来:

   @tailrec
   def split(
     in: List[Char], 
     next: List[Char] = Nil, 
     out: List[List[Char]] = Nil
   ): List[List[Char]] = in match { 
       case Nil => out.map(_.reverse).reverse
       case ' ' :: tail  => split(tail,  Nil, next :: out)
       case head :: tail => split(tail, head :: next, out)
     }

这里有几个常见的技巧是将构建的实际结果传递给递归调用,而不是仅仅返回它(这样函数可以优化为 tail-recursive - 不会需要 space 在堆栈上),而且,你以相反的方向构建你的列表,将元素添加到它们之前,而不是追加,然后在列表完成时在最后反转(这允许实现保持线性,因为添加到列表之前是常数时间,不像追加,它是 O(N),使整个事情成为二次的(讨厌!))。