使用scala集合 - CanBuildFrom麻烦

时间:2012-08-30 20:22:02

标签: scala collections

我正在尝试编写一个方法,它接受任何类型的集合CC[_]并将其映射到一个新的集合(相同的集合类型,但是不同的元素类型),我正在挣扎。基本上我正在尝试实现map不在集合本身上

问题

我正在尝试使用看起来有点像签名的方法来实现:

def map[CC[_], T, U](cct: CC[T], f: T => U): CC[U]

它的用法是:

map(List(1, 2, 3, 4), (_ : Int).toString) //would return List[String]

我对CCArray的问题感兴趣,我对我的尝试(下方)最终无效的原因感兴趣。


我的尝试

(对于不耐烦的人,在接下来的内容中,我完全没有让它发挥作用。重申一下,问题是“我怎么能写出这样的方法?”)

我是这样开始的:

scala> def map[T, U, CC[_]](cct: CC[T], f: T => U)(implicit cbf: CanBuildFrom[CC[T], U, CC[U]]): CC[U] = 
     | cct map f
                                                             ^
 <console>:9: error: value map is not a member of type parameter CC[T]
       cct map f
           ^

好的,这是有道理的 - 我需要说CC是可以穿越的!

scala> def map[T, U, X, CC[X] <: Traversable[X]](cct: CC[T], f: T => U)(implicit cbf: CanBuildFrom[CC[T], U, CC[U]]): CC[U] = 
     | cct map f
<console>:10: error: type mismatch;
 found   : Traversable[U]
 required: CC[U]
       cct map f
           ^

呃,好的!也许如果我实际指定cbf实例。毕竟,它将返回类型(To)指定为CC[U]

scala> def map[T, U, X, CC[X] <: Traversable[X]](cct: CC[T], f: T => U)(implicit cbf: CanBuildFrom[CC[T], U, CC[U]]): CC[U] = 
     | cct.map(t => f(t))(cbf)
<console>:10: error: type mismatch;
 found   : scala.collection.generic.CanBuildFrom[CC[T],U,CC[U]]
 required: scala.collection.generic.CanBuildFrom[Traversable[T],U,CC[U]]
       cct.map(t => f(t))(cbf)
                          ^

呃,好的!这是一个更具体的错误。看起来我可以使用它!

scala> def map[T, U, X, CC[X] <: Traversable[X]](cct: CC[T], f: T => U)(implicit cbf: CanBuildFrom[Traversable[T], U, CC[U]]): CC[U] = 
     | cct.map(t => f(t))(cbf)
map: [T, U, X, CC[X] <: Traversable[X]](cct: CC[T], f: T => U)(implicit cbf: scala.collection.generic.CanBuildFrom[Traversable[T],U,CC[U]])CC[U]

辉煌。我有一个map!让我们用这个东西!

scala> map(List(1, 2, 3, 4), (_ : Int).toString)
<console>:11: error: Cannot construct a collection of type List[java.lang.String] with elements of type java.lang.String based on a collection of type Traversable[Int].
              map(List(1, 2, 3, 4), (_ : Int).toString)
                 ^

说,什么?


观察

我真的情不自禁地认为Tony Morris当时对此的观察是绝对的。他说什么?他说“不管是什么,它不是地图”。看看这在scalaz风格中是多么容易

scala> trait Functor[F[_]] { def fmap[A, B](fa: F[A])(f: A => B): F[B] }
defined trait Functor

scala> def map[F[_]: Functor, A, B](fa: F[A], f: A => B): F[B] = implicitly[Functor[F]].fmap(fa)(f)
map: [F[_], A, B](fa: F[A], f: A => B)(implicit evidence$1: Functor[F])F[B]

然后

scala> map(List(1, 2, 3, 4), (_ : Int).toString)
<console>:12: error: could not find implicit value for evidence parameter of type Functor[List]
              map(List(1, 2, 3, 4), (_ : Int).toString)
                 ^

那样

scala> implicit val ListFunctor = new Functor[List] { def fmap[A, B](fa: List[A])(f: A => B) = fa map f }
ListFunctor: java.lang.Object with Functor[List] = $anon$1@4395cbcb

scala> map(List(1, 2, 3, 4), (_ : Int).toString)
res5: List[java.lang.String] = List(1, 2, 3, 4)

回忆自己:听托尼!

3 个答案:

答案 0 :(得分:23)

您遇到的问题不一定是CanBuildFrom本身,也不一定是ArraySeq问题。您已经遇到String 更高的角色,但支持map对其Char

SO:首先是对Scala集合设计的偏离。

您需要的是一种推断集合类型(例如StringArray[Int]List[Foo])和元素类型(例如Char,{{1 }},Int对应于上述内容。

Scala 2.10.x添加了一些“类型类”来帮助您。例如,您可以执行以下操作:

Foo

这里有两件。首先,使用集合的类需要两个类型参数:集合的特定类型class FilterMapImpl[A, Repr](val r: GenTraversableLike[A, Repr]) { final def filterMap[B, That](f: A => Option[B])(implicit cbf: CanBuildFrom[Repr, B, That]): That = r.flatMap(f(_).toSeq) } implicit def filterMap[Repr, A](r: Repr)(implicit fr: IsTraversableOnce[Repr]): FilterMapImpl[fr.A,Repr] = new FilterMapImpl(fr.conversion(r)) 和元素类型Repr

接下来,您定义一个仅采用集合类型A的隐式方法。您可以使用Repr(注意:还有一个IsTraversableOnce)来捕获该集合的元素类型。您会在类型签名IsTraversableLike中看到此内容。

现在,部分原因是因为Scala不会对其所有类似“仿函数”的操作使用相同的类别。具体而言,FilterMapImpl[Repr, fr.A]map的有用方法。我可以调整所有角色。但是,String只能是String。如果我想定义Seq[Char],那么我的类别只能包含Functor类型和箭头Char。该逻辑在Char => Char中捕获。但是,由于CanBuildFromString,如果您尝试在Seq[Char]的{​​{1}}方法支持的类别中使用map,那么{{ 1}}会改变您对Seq的调用。

我们实质上是为我们的类别定义“继承”关系。如果您尝试使用map模式,我们会将类型签名删除到我们可以保留的最具体的类别。称之为你的意思;这对当前的收藏设计来说是一个很大的激励因素。

结束弃儿,回答问题

现在,因为我们试图同时推断类型的 lot ,我认为此选项具有最少的类型注释:

CanBuildFrom

这里需要注意的重要一点是map抓住了从Functorimport collection.generic._ def map[Repr](col: Repr)(implicit tr: IsTraversableLike[Repr]) = new { def apply[U, That](f: tr.A => U)(implicit cbf: CanBuildFrom[Repr, U, That]) = tr.conversion(col) map f } scala> map("HI") apply (_ + 1 toChar ) warning: there were 2 feature warnings; re-run with -feature for details res5: String = IJ 的转化,允许您使用IsTraversableLike方法。

选项2

我们还将方法调用分开一点,以便在我们定义匿名函数之前,Scala可以推断类型ReprTraversableLike。为避免匿名函数上的类型注释,我们必须在显示之前将所有类型已知。现在,我们仍然可以让Scala推断某些类型,但如果我们这样做,则会丢失隐式 map的内容:

Repr

请注意,我们必须使用U。似乎大多数F-bounded类型都需要这种杂耍。

在任何情况下,现在让我们看看在Traversable扩展的内容上会发生什么:

import collection.generic._
import collection._
def map[Repr <: TraversableLike[A, Repr], A, U, That](col: Repr with TraversableLike[A,Repr])(f: A => U)(implicit cbf: CanBuildFrom[Repr, U, That]) = 
    col map f

那很好。但是,如果我们想要Repr with TraversableLike[A,Repr]Traversable使用相同的用法,我们还需要做更多的工作:

scala> map(List(40,41))(_ + 1 toChar )
warning: there were 1 feature warnings; re-run with -feature for details
res8: List[Char] = List(), *)

此用法分为两部分:

  1. 我们必须使用类型注释来隐式转换Array / Stringscala> map(Array('H', 'I'): IndexedSeq[Char])(_ + 1 toChar)(breakOut): Array[Char] warning: there were 1 feature warnings; re-run with -feature for details res14: Array[Char] = Array(I, J) scala> map("HI": Seq[Char])(_ + 1 toChar)(breakOut) : String warning: there were 1 feature warnings; re-run with -feature for details res11: String = IJ / String
  2. 我们必须对Array使用Seq并输入预期返回值。
  3. 这完全是因为IndexedSeq类型不包含breakOutCanBuildFrom,因为它们使用隐式转化。

    选项3

    您可以将所有隐含放在最后,并要求用户注释类型。不是最优雅的解决方案,所以我想我会避免发布它,除非你真的想看到它。

    所以,基本上如果你想把Repr <: TraversableLike[A,Repr]String作为集合,你必须跳过一些箍。此地图的类别限制适用于Scala中的ArrayString仿函数。

    我希望有所帮助。如果你还有其他问题,请告诉我。

答案 1 :(得分:11)

实际上有几个问题......

让我们从你的最后一次尝试开始:

scala> def map[T, U, X, CC[X] <: Traversable[X]](cct: CC[T], f: T => U)
 (implicit cbf: CanBuildFrom[Traversable[T], U, CC[U]]): CC[U] = 
  cct.map(t => f(t))(cbf)

这个确实编译但不起作用,因为根据你的类型签名,它必须在范围内寻找一个隐含的CanBuildFrom[Traversable[Int], String, List[String]],而不是一个。如果您要手动创建一个,它将起作用。

现在是先前的尝试:

scala> def map[T, U, X, CC[X] <: Traversable[X]](cct: CC[T], f: T => U)
 (implicit cbf: CanBuildFrom[CC[T], U, CC[U]]): CC[U] = 
  cct.map(t => f(t))(cbf)
<console>:10: error: type mismatch;
 found   : scala.collection.generic.CanBuildFrom[CC[T],U,CC[U]]
 required: scala.collection.generic.CanBuildFrom[Traversable[T],U,CC[U]]
       cct.map(t => f(t))(cbf)
                          ^

这个不能编译,因为CanBuildFrom中的隐式Traversable被硬编码为仅接受Traversable作为From集合。但是,正如在另一个答案中所指出的,TraversableLike知道实际的集合类型(它是它的第二个类型参数),因此它使用适当的map定义CanBuildFrom[CC[T], U, CC[U]]并且每个人都很高兴。实际上,TraversableLikemap继承了此scala.collection.generic.FilterMonadic方法,因此这更为通用:

scala> import scala.collection.generic._
import scala.collection.generic._

scala> def map[T, U, CC[T] <: FilterMonadic[T, CC[T]]](cct: CC[T], f: T => U)
 |  (implicit cbf:  CanBuildFrom[CC[T], U, CC[U]]): CC[U] = cct.map(f)
warning: there were 1 feature warnings; re-run with -feature for details
map: [T, U, CC[T] <: scala.collection.generic.FilterMonadic[T,CC[T]]](cct: CC[T], f: T => U)(implicit cbf: scala.collection.generic.CanBuildFrom[CC[T],U,CC[U]])CC[U]

scala> map(List(1,2,3,4), (_:Int).toString + "k")
res0: List[String] = List(1k, 2k, 3k, 4k)

最后,上述内容不适用于数组,因为Array不是FilterMonadic。但是存在从ArrayArrayOps的隐式转换,后者实现FilterMonadic。因此,如果你在那里添加一个视图绑定,你也可以获得适用于数组的东西:

scala> import scala.collection.generic._
import scala.collection.generic._

scala> def map[T, U, CC[T]](cct: CC[T], f: T => U)
 |  (implicit cbf:  CanBuildFrom[CC[T], U, CC[U]], 
 |   ev: CC[T] => FilterMonadic[T,CC[T]]): CC[U] = cct.map(f)
warning: there were 1 feature warnings; re-run with -feature for details
map: [T, U, CC[T]](cct: CC[T], f: T => U)(implicit cbf: scala.collection.generic.CanBuildFrom[CC[T],U,CC[U]], implicit ev: CC[T] => scala.collection.generic.FilterMonadic[T,CC[T]])CC[U]

scala> map(List(1,2,3,4), (_:Int).toString + "k")
res0: List[String] = List(1k, 2k, 3k, 4k)

scala> map(Array(1,2,3,4), (_:Int).toString + "k")
res1: Array[String] = Array(1k, 2k, 3k, 4k)

修改 还有一种方法可以使它适用于String和co:只需删除输入/输出集合中的较高种类,使用中间的第三个:

def map[T, U, From, To, Middle](cct: From, f: T => U)
 (implicit ev: From => FilterMonadic[T, Middle], 
  cbf: CanBuildFrom[Middle,U,To]): To = cct.map(f)

这适用于String甚至Map[A,B]

scala> map(Array(42,1,2), (_:Int).toString)
res0: Array[java.lang.String] = Array(42, 1, 2)

scala> map(List(42,1,2), (_:Int).toString)
res1: List[java.lang.String] = List(42, 1, 2)

scala> map("abcdef", (x: Char) => (x + 1).toChar)
res2: String = bcdefg

scala> map(Map(1 -> "a", 2 -> "b", 42 -> "hi!"), (a:(Int, String)) => (a._2, a._1))
res5: scala.collection.immutable.Map[String,Int] = Map(a -> 1, b -> 2, hi! -> 42)

用2.9.2测试。但正如jsuereth指出的那样,2.10中的精彩IsTraversableLike更适合这一点。

答案 2 :(得分:8)

这是吗?

def map[A,B,T[X] <: TraversableLike[X,T[X]]]
  (xs: T[A])(f: A => B)(implicit cbf: CanBuildFrom[T[A],B,T[B]]): T[B] = xs.map(f)

map(List(1,2,3))(_.toString)
// List[String] = List(1, 2, 3)

另见this question