Scala中的常规理解

4
据我所理解,Scala 的 for 推导符号依赖于第一个产生器来定义如何组合元素。即,for (i <- list) yield i 返回列表并且 for (i <- set) yield i 返回集合。
我想知道是否有一种方法可以独立于第一个生成器的属性来指定如何组合元素。例如,我想要获取给定列表中的所有元素的集合,或者给定集合中所有元素的总和。我找到的唯一方法是首先按照 for 推导符号的规定构建一个列表或集合,然后对其应用转换函数-在此过程中构建一个无用的数据结构。
我心目中的是一种通用的“代数”推导符号,例如 Ateji PX 中存在的。
`+ { i | int i : set }               // the sum of all elements from a given set
set() { i | int i : list }           // the set of all elements from a given list
concat(",") { s | String s : list }  // string concatenation with a separator symbol

这里的第一个元素(`+`,`set()`,`concat(",")`)是所谓的“幺半群”,它定义了元素如何组合,独立于第一个生成器的结构(可以有多个生成器和过滤器,我只是试图保持例子简洁)。
有什么想法可以在Scala中实现类似的结果,同时保持简洁明了的符号表示?据我所知,for-comprehension符号已经在编译器中被硬编码,无法升级。
感谢您的反馈。
4个回答

12

关于for循环推导式

在Scala中,for循环推导式是对flatMapfiltermapforeach方法的语法糖。与调用这些方法一样,目标集合的类型决定了返回集合的类型。

list map f   //is a List
vector map f // is a Vector

这个属性是Scala集合库的其中一个基本设计目标,并且在大多数情况下都是可取的。

回答问题

当然,您不需要构建任何中间集合:

(list.view map (_.prop)).toSet //uses list.view

(list.iterator map (_.prop)).toSet //uses iterator

(for { l <- list.view} yield l.prop).toSet //uses view

(Set.empty[Prop] /: coll) { _ + _.prop } //uses foldLeft

所有的yield都不会生成不必要的集合。个人偏好第一个方案。在使用符合惯用法的Scala集合操作时,每个“集合”都带有这些方法:

//Conversions
toSeq
toSet
toArray
toList
toIndexedSeq
iterator
toStream

//Strings
mkString

//accumulation
sum 

在集合的元素类型具有一个隐式的Numeric实例的情况下,可以使用last方法,例如:

Set(1, 2, 3, 4).sum //10
Set('a, 'b).sum //does not compile

请注意,Scala中字符串拼接的例子如下:

list.mkString(",")

scalaz FP库中,可能看起来像这样(使用Monoid来汇总字符串):

list.intercalate(",").asMA.sum

你的建议看起来与Scala毫不相似;我不确定它们是否受到其他语言的启发。


4

foldLeft?这就是你所描述的。

给定集合中所有元素的总和:

(0 /: Set(1,2,3))(_ + _)

给定列表中所有元素的集合
(Set[Int]() /: List(1,2,3,2,1))((acc,x) => acc + x)

使用分隔符符号进行字符串拼接:

("" /: List("a", "b"))(_ + _) // (edit - ok concat a bit more verbose:
("" /: List("a", "b"))((acc,x) => acc + (if (acc == "") "" else ",")  + x)

(0 /: Set(1,2,3))(_ + _) 更习惯地写作 Set(1,2,3).foldLeft(0)(_ + _) 或者更好的是 Set(1,2,3).sum。后一种形式是唯一真正声明式的构造,也是唯一在并行集合面前能够工作的形式。同样,(Set[Int]() /: List(1,2,3,2,1))((acc,x) => acc + x) 最好写成 List(1,2,3,2,1).toSet - Kevin Wright
此外,("" /: List("a", "b"))((acc,x) => acc + (if (acc == "") "" else ",") + x) 更好地编写为 List("a", "b") mkString "," - Kevin Wright
@Kevin,这个问题是关于使用哪种结构来开始第一个元素,将其提供给一个或多个生成器,并使用函数将它们组合起来。对于一个生成器,foldLeft适合这个描述。sumtoSet存在的事实并不是我的重点,因为oxbow_lakes已经提到了这些内容。 - huynhjl

1

您还可以通过显式提供隐式的CanBuildFrom参数作为scala.collection.breakout并指定结果类型来强制for推导的结果类型。

考虑以下REPL会话:

scala> val list = List(1, 1, 2, 2, 3, 3)
list: List[Int] = List(1, 1, 2, 2, 3, 3)

scala> val res = for(i <- list) yield i
res: List[Int] = List(1, 1, 2, 2, 3, 3)

scala> val res: Set[Int] = (for(i <- list) yield i)(collection.breakOut)
res: Set[Int] = Set(1, 2, 3)

如果不明确指定CanBuildFrom,会导致类型错误:

scala> val res: Set[Int] = for(i <- list) yield i
<console>:8: error: type mismatch;
 found   : List[Int]
 required: Set[Int]
       val res: Set[Int] = for(i <- list) yield i
                                 ^

为了更深入地理解这个问题,我建议阅读以下内容:

http://www.scala-lang.org/docu/files/collections-api/collections-impl.html


0
如果你想使用for循环并且仍然能够将你的值组合成某个结果值,你可以按照以下步骤操作。
case class WithCollector[B, A](init: B)(p: (B, A) => B) {
  var x: B = init
  val collect = { (y: A) => { x = p(x, y) } }
  def apply(pr: (A => Unit) => Unit) = {
    pr(collect)
    x
  }
}

// Some examples
object Test {

  def main(args: Array[String]): Unit = {

    // It's still functional
    val r1 = WithCollector[Int, Int](0)(_ + _) { collect =>
      for (i <- 1 to 10; if i % 2 == 0; j <- 1 to 3) collect(i + j)
    }

    println(r1) // 120

    import collection.mutable.Set

    val r2 = WithCollector[Set[Int], Int](Set[Int]())(_ += _) { collect =>
      for (i <- 1 to 10; if i % 2 == 0; j <- 1 to 3) collect(i + j)
    }

    println(r2) // Set(9, 10, 11, 6, 13, 4, 12, 3, 7, 8, 5)
  }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接