Scala源代码:如何理解zipWithIndex

时间:2018-11-22 22:25:39

标签: scala function typeclass

我想实现我的自定义函数minWithIndex,所以我研究了scala library中的 zipWithIndex 函数。 我无法理解。

我知道函数的功能,但是无法弄清楚它是如何执行的。 预先感谢

def zipWithIndex[A1 >: A, That](implicit bf: CanBuildFrom[Repr, (A1, Int), That]): That = {
    val b = bf(repr)
    var i = 0
    for (x <- this) {
      b += ((x, i))
      i += 1
    }
    b.result()
  }

2 个答案:

答案 0 :(得分:1)

您的问题实际上看起来像是两个不同的问题:

  1. Scala标准库集合的设计方式,尤其是zipWithIndex的工作方式?

  2. 如何实现我的自定义minWithIndex

不幸的是,第一个问题可能比第二个问题复杂。

Scala集合设计

从2.12版开始,当前的scala.collection的设计非常灵活,但要付出代价,要使用许多高级功能和/或设计模式。还有人可能会争辩说,它在实际使用中是过度设计的,因此对于Scala新手来说很难理解。

设计馆藏库是一个众所周知的难题,因为理想情况下,您希望捕获许多不同的方面。主要的事情是您可能希望捕获尽可能多的方面,但同时又要尽可能减少代码重复。 (例如,考虑链接列表与基于数组的列表。两者都应具有可以像indexOfsize那样实现的getByIndex之类的方法,因此理想情况下,您希望拥有这里只有一个实现。)这是一个真正的设计挑战。

以下是影响Scala的馆藏设计的各个方面的详尽列表(您可能还会在早期(2.8)版本here上找到一些设计说明):

  • 用于实现相似功能的不同数据结构的统一接口(例如,链表与基于数组的列表非常相似,而链表与基于树的集合则不太相似,但仍具有一些共同点)。这就是SeqGenTraversableLike等各种特征和深层继承层次的原因。

  • 类型安全。我们希望整数列表与字符串列表的类型不同。而且我们想知道存储元素的类型。这就是为什么所有集合都是通用的。

  • 性能。在每种语言中,标准集合都是最常用的代码之一,因此良好的性能非常重要。这就是为什么在几个地方对于纯不可变FP接口存在“不纯”可变实现的原因。

  • 单独的只读和可变集合(在FP语言中特别重要,在其他语言中也特别重要)。这就是为什么有scala.collectionscala.collection.mutablescala.collection.immutable软件包的原因。

  • 支持潜在的无限序列,例如生成的(斐波那契数)或来自外部源的序列(例如,从控制台输入读取的字节序列)。这就是诸如TraversableOnceIterable之类的原因。

  • 支持无法(轻松)处理两次的序列。例如,某个主要证券交易所上的所有交易流是如此之大,以至于无法存储和重新处理。这就是TraversableOnceTraversable

  • 的原因
  • 内部与外部迭代(这是IterableTraversable之间分离的原因)

  • 急速计算与惰性计算(这是“ View”后缀类型的原因)

  • 支持不是集合的类似集合的对象。例如,您正在实现HTML解析器甚至浏览器。许多HTML元素可以有子级,但是成为子级的集合并不是主要责任。这就是带有'Gen`前缀的各种特征的原因。

现在让我们回到zipWithIndex。这是许多方法中的一种(其他类似方法是mapfitler和其他方法),这对设计人员构成了额外的挑战:这些方法从当前方法中产生了新的收藏。一方面,这样的方法可以一次通用地用于所有集合,另一方面,如果使用朴素的实现,我们将丢失特定的类型,更糟糕的是,我们可能被迫更改实现,从而可能会更改语义。考虑一下filter方法的这种天真的实现:

trait Iterable[A] {
  def filter(p: A => Boolean): ? = {
    var res: List[A] = Nil
    for (x <- this)
      if (p(x)) res = x :: res
    res
  }
}

要放置哪种返回类型而不是??如果仅放置Iterable[A],则意味着我们立即失去了使用List中不可用的Iterable的所有方法的能力(例如按索引访问)。但是另一件事是,如果我们的Iterable实际上是Set,该怎么办。我们可能希望filter的结果再次成为Set,而不是List。这就是上面的参考文献所称的“ 相同结果类型原理 ”。 Scala是为数不多的几种语言之一,可让您以最小的代码重复来设计一个优雅的解决方案。主要思想是每个实现都有一个伴随对象(例如Iterableimmutable.List),每个中间特征都具有一个伴随特征(例如SeqFactory)。这些同伴提供的功能是可以创建“相同”集合的 new 实例(可能具有不同的通用类型)。

所引用的文章中最后没有提到的是CanBuildFrom隐式参数。其背后的逻辑如下:假设您有一个链表(List),并希望将带有索引的基于数组的列表(Vector)压缩。您是否需要在此过程中创建带有索引的中间链接列表? CanBuildFrom的诀窍是:不,您不必这样做。隐式参数是Scala的一项相当高级的功能,如果您还不熟悉,则可能应该阅读更多内容。这个想法是编译器可以自动搜索匹配类型的参数。因此,如果有任何匹配类型的值是作用域,则代码将进行编译。这意味着CanBuildFrom的存在可以用作证据,您可以在进行某些数据操作的同时更改集合的基础数据结构(zipWithIndexmapfilter等)。每个伴随对象都提供具有相同目标数据结构的默认CanBuildFrom值(例如,参见immutable.List.canBuildFrom)。

因此,现在我们可以了解如何实现IterableLike.zipWithIndex(这是所有子类型的默认实现)

  def zipWithIndex[A1 >: A, That](implicit bf: CanBuildFrom[Repr, (A1, Int), That]): That = {
    val b = bf(repr)
    var i = 0
    for (x <- this) {
      b += ((x, i))
      i += 1
    }
    b.result()
  }

签名表明,使用zipWithIndex可以将Repr的集合A转换成That-元组(A1, Int)的集合,只要

  1. A1A的超类型(这是安全的上流)
  2. 您有证据表明可以从ThatRepr对象)构建bf

该方法的作用是要求bf证据创建b-一个新的Builder[(A1, Int), That],然后用元组填充生成器,最后返回That从构建器(b.result()中收集。出于性能原因,同时使用了builder和带有for的{​​{1}}。

如何实现自己的minWithIndex

答案取决于您希望它有多通用,以及多少性能对您很重要。以下是一些选项:

var i

请注意,此处使用implicit class MinWithIndexOps[A, Repr <: IterableLike[A, Repr]](it: IterableLike[A, Repr]) { def minWithIndexWithZip[B >: A](implicit cmp: Ordering[B], bf: CanBuildFrom[Repr, (A, Int), Iterable[(A, Int)]]): (A, Int) = it.zipWithIndex(bf).min(Ordering.by[(A, Int), B]((kv: (A, Int)) => kv._1)) def minWithIndexWithFold[B >: A](implicit cmp: Ordering[B]): (A, Int) = { if (it.isEmpty) throw new UnsupportedOperationException("empty.min") val h = it.head val res = it.foldLeft((h, 0, 0))((acc, cur) => acc match { case (minVal, minIndex, curIndex) => if (cmp.lteq(minVal, cur)) (minVal, minIndex, curIndex + 1) else (cur, curIndex, curIndex + 1) }) (res._1, res._2) } def minWithIndexWithVars[B >: A](implicit cmp: Ordering[B]): (A, Int) = { if (it.isEmpty) throw new UnsupportedOperationException("empty.min") var minVal = it.head var minIndex = 0 var i = 0 for (cur <- it) { if (cmp.gt(minVal, cur)) { minVal = cur minIndex = i } i += 1 } (minVal, minIndex) } } def test(): Unit = { val data = "qwerty" :: "asdfg" :: "zxcvb" :: Nil println(data.minWithIndexWithZip) println(data.minWithIndexWithFold) println(data.minWithIndexWithVars) } 关键字以不同的含义来创建一个implicit,该关键字可以使用新方法有效地扩展implicit class的每个实例。

第一个实现IterableLike非常简单,但效率可能很低:您实际上是先执行minWithIndexWithZip然后调用zipWithIndex(另请注意,标准min在抛出异常而不返回min的Scala约定,我在其他实现中使用了相同的语义)。这种方法效率低下,因为您必须创建整个新集合才能将其立即处置。

Option[A]是不依赖于minWithIndexWithFold而使用zipWithIndex的另一种实现。 foldLeft同时是一个非常基本且非常通用的操作。该代码的一个好处是它也是不可变的。但是由于这个原因,它的性能也不佳:将立即创建并处置许多中间累加器对象。

最后一个fold可能是最“纯”的,但性能最高的版本,它使用简单的类似命令式的代码。

答案 1 :(得分:0)

我看到scala有时在内部使用可变解决方案。与 import 'package:flutter/material.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter_webview_plugin/flutter_webview_plugin.dart'; import 'package:share/share.dart'; class Everything extends StatefulWidget { @override State<StatefulWidget> createState() { // TODO: implement createState return _EverythingState(); } } class _EverythingState extends State<Everything> { Widget _buildEventCards(BuildContext context, DocumentSnapshot document) { var width = MediaQuery.of(context).size.width; return Container( padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 14.0), child: Card( elevation: 0.0, child: Column( children: <Widget>[ Image.asset( document['image'], fit: BoxFit.cover, ), Container( padding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0), child: Column( children: <Widget>[ SizedBox( height: 15.0, ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ // Icon(document['icon']), ], ), ], ), SizedBox( height: 10.0, ), Wrap( direction: Axis.horizontal, alignment: WrapAlignment.start, runAlignment: WrapAlignment.start, children: <Widget>[ Chip( backgroundColor: Colors.grey[100], label: Text( document['date'], ), ), SizedBox( width: 10.0, ), Chip( backgroundColor: Colors.grey[100], label: Text(document['source']), ), SizedBox( width: 10.0, ), Chip( backgroundColor: Colors.grey[100], label: Text( document['location'], ), ), ], ), SizedBox( height: 15.0, ), Container( width: width, child: Text( document['title'], style: TextStyle(fontSize: 24.0), ), ), SizedBox( height: 20.0, ), Text(document['desc']), SizedBox( height: 20.0, ), Row( children: <Widget>[ FlatButton( child: Text( 'More', style: TextStyle( color: Color.fromRGBO(118, 190, 208, 1.0)), ), color: Colors.grey[100], onPressed: () { print('webview clicked'); Navigator.of(context).push( MaterialPageRoute( builder: (context) => WebviewScaffold( url: document['url'], appBar: AppBar( title: Text(document['source']), backgroundColor: Color.fromRGBO( 135, 142, 136, 1.0, ), ), ), ), ); }, ), SizedBox( width: 15.0, ), new MyButton(), ], ), ], ), ), ], ), ), ); } @override Widget build(BuildContext context) { return StreamBuilder( stream: Firestore.instance.collection('stories').snapshots(), builder: (context, snapshot) { if (!snapshot.hasData) return const Text('Loading...'); return ListView.builder( itemCount: snapshot.data.documents.length, itemBuilder: (context, index) => _buildEventCards(context, snapshot.data.documents[index]), ); }); } // @override // void initState() { // // TODO: implement initState // super.initState(); // FirebaseAdMob.instance.initialize(appId: FirebaseAdMob.testAppId); // var bannerAd = _buildBanner()..load(); // } } class MyButton extends StatelessWidget { const MyButton({ Key key, }) : super(key: key); @override Widget build(BuildContext context) { return FlatButton( child: Text( 'Share', style: TextStyle( color: Color.fromRGBO(245, 93, 62, 1.0)), ), color: Colors.grey[100], onPressed: () { final RenderBox box = context.findRenderObject(); Share.share('Hello this is a test', sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size); }, ); } } 的实现相同。

基本上是:

  • 遍历集合(zipWithIndex循环)
  • 为每个元素创建一个tuple for
  • 将它们附加到可变的可变缓冲区((a, Int)

因此,对于输入b += ((x, i)),您会在List("first", "second")之后得到List(("first", 1), ("second", 2))

zipWithIndex

您可以使用具有不变性的递归方法来解决相同的问题

scala> List("first", "second").zipWithIndex
res3: List[(String, Int)] = List((first,0), (second,1))