Coursera公开课Functional Programming Principles in Scala习题解答:Week 三

Coursera公开课Functional Programming Principles in Scala习题解答:Week 3

引言

这周的作业其实有点复杂,需要完成的代码有点多,有点绕。本周的课程主要讲了Scala中的类、继承和多态,作业也很好的从各个方面考察了课程的内容。作业题目工程主要需要完成的部分是TweetSet.scala这个文件中的内容,比较新潮,都是和推特相关。其中定义了一个抽象类TweetSet,以及其的两个子类Empty、NonEmpty,表示空集和非空集。非空集使用二叉树来表示,二叉树的根是一个Tweet类对象,表示一条推特(用天朝的话来说叫做“微博”),一条微博哦不,一条推特包含user、text和retweets三个字段来标识,分别表示此条推的作者、内容以及转发数量。二叉树的左右子树是按照其根的text内容来排序的,左子树所有节点的text在字典序上都小于根节点的text,右子树所有节点的text在字典序上都大于根节点的text。

并注意,所有的类都是immutable类,也就是说所有的操作不会改变自身的内容,而是返回一个经过修改之后的新对象。

题目强调了在做作业之前可以参考一下已经实现的方法contains和incl。我们可以看一下Empty和NonEmpty类中这两个方法的实现:

 def contains(tweet: Tweet): Boolean = false

  def incl(tweet: Tweet): TweetSet = new NonEmpty(tweet, new Empty, new Empty)
 
def contains(x: Tweet): Boolean =
    if (x.text < elem.text) left.contains(x)
    else if (elem.text < x.text) right.contains(x)
    else true

  def incl(x: Tweet): TweetSet = {
    if (x.text < elem.text) new NonEmpty(elem, left.incl(x), right)
    else if (elem.text < x.text) new NonEmpty(elem, left, right.incl(x))
    else this
  }

以上两段分别是Empty和NonEmpty两个类中contains和incl的实现代码,而它们的父类中相应的方法声明是这样的:

  def incl(tweet: Tweet): TweetSet

  /**
   * Returns a new `TweetSet` which excludes `tweet`.
   */
  def remove(tweet: Tweet): TweetSet

  /**
   * Tests if `tweet` exists in this `TweetSet`.
   */
  def contains(tweet: Tweet): Boolean

我们可以看到在TweetSet中,所有的方法都没有函数体,我们并不需要在抽象类中实现这些方法,因为我们并不会实例化一个抽象类,所以即便实现了方法,也没有调用他们的机会。

NonEmpty中两个方法的实现很简单,因为Empty中不包含任何元素,所以对于所有的contains的方法,只需要返回false即可。而incl方法是在Empty中增加一个元素,空集中增加一个元素之后,就变成了一个NonEmpty,其根是新增加的Tweet对象,而左右子树分别为空。

再来看NonEmpty,contains方法首先判断其根的text和入参的text的字典序关系,如果入参的text小于根的text,说明和入参相同的text只能存在于这个集合的左子树中;反之存在于集合的右子树中。如果入参的text和根的text相同,则返回true。

有没有注意到,判断是否包含其实只是判断了集合中是否有元素的text和入参的相同,而user和retweet两个都没有判断(这里你可以理解为一个bug,但我们认为这是题目出于简单实现的初衷)。

再来看incl方法,如果入参的text小于集合根的text,则将其添加到左子树中,否则加入到右子树中。如果入参的text等于集合的text则表示此元素已经存在于集合中,不需要添加,返回本集合即可。

这里还有一个细节,就是incl不会修改本集合的内容,而是在本集合上做一些操作,然后返回编辑之后的一个新对象(这和题目开始的要求一致)。

Filtering

好了,看完了集合已经实现的两个方法,我们就来分析作业的第一项内容:filtering。

这部分要实现一个filter方法,其入参为一个由一个Tweet对象到boolean值的判定函数,返回一个集合的子集,其中子集中的所有元素都被入参的判定函数判定为true,比如以下这个调用:

tweets.filter(tweet => tweet.retweets > 10)

将返回tweets中所有retweets大于10的元素的集合。

题目给出了一个提示,可以为filter方法添加一个辅助方法filterAcc,两个函数的原型如下:

/** This method takes a predicate and returns a subset of all the elements
 *  in the original set for which the predicate is true.
 */
def filter(p: Tweet => Boolean): TweetSet
def filterAcc(p: Tweet => Boolean, acc: TweetSet): TweetSet


对于Empty来说,这个方法实现很简单,因为Empty中不包含任何元素,所以对于任何判定函数,Empty的自己都是自身,所以返回this即可。

接下来看一下NonEmpty怎么实现这个方法。从前面contains和incl两个方法的模式就可以看出来,一个需要遍历集合才能完成的方法,通常都是先对二叉树的根做操作,其次对二叉树的左右子树分别作操作,这是一个递归遍历的思想。那么具体到filter方法上呢,我们仍然可以先判断集合的根元素是否符合判定函数的条件,如果符合则将其加入我们要返回的集合,接下来就要对左右子树分别作filter操作,这里的左右子树都可以当成一个独立的集合来做,左右子树的左右子树又可以重复之前的操作,这里看起来是否有点熟悉呢?(子又有孙,孙又有子,子子孙孙无穷匮也……)

好吧,那我们需要用一个东东来保存当前已经遍历过的并且符合判定函数的元素,没错,你看到filterAcc的第二个入参为一个TweetSet集合,这正中下怀啊!

所以在filter函数中需要调用filterAcc,因为初始的时候acc中并没有元素,所以这里给它传递一个Empty对象进去。p是判定函数,在传递过程中不能改变。

接下来的关键就是filterAcc怎么实现了,在一个集合中的filterAcc需要首先判断其根,也即是elem是否符合判定函数p,如果符合该怎么办呢?没错,就是将其加入acc,加入的操作可以使用本来已经实现的incl方法来实现。当然,判断完根元素之后,还需要对左右子树分别做同样的操作。需要注意的是在左右子树调用filterAcc时,不要丢弃了之前已经积攒的acc集合,每次调用filterAcc,都需要将acc做修改之后的新集合作为flterAcc的第二个参数传递进去。

本来逻辑比较难,但是题目给出了filterAcc这个提示之后,我们的思路就开阔了很多。这里还是不能忘记每次返回的方法都要是immutable的,也就是每次要返回一个新创建的集合!

Taking Unions

作业的第二个任务是完成union方法,这个方法的入参是另外一个TweetSet对象。union方法就要返回调用者和参数的并集,其签名如下:

def union(that: TweetSet): TweetSet

有了前面的铺垫,这边应该很简单就能想到solution了。同样分成两个部分来讲,第一个是Empty,对于空集来说,空集和任何一个集合的并集都是参数所代表的那个集合,所以直接返回参数表示的集合就可以了。

NonEmpty来说,按照之前的思路,需要先将对应二叉树的跟加入,再一次将左右子树作为union的参数传入已经加入了二叉树根的集合。当然每次都需要保存调用集合前一次操作后得到的集合作为下一步操作的参数。

很简单啦,去实现吧。

Sorting Tweets by Their Influence

这一部分是完成对一个TweetSet的排序,返回一个TweetList。TweetSet.scala中也定义了一个特质TweetSet,表示一个列表:

trait TweetList {
  def head: Tweet
  def tail: TweetList
  def isEmpty: Boolean
  def foreach(f: Tweet => Unit): Unit =
    if (!isEmpty) {
      f(head)
      tail.foreach(f)
    }
}

我们可以看到这是一个列表的典型表示方法,head为一个Tweet类对象,tail是除掉head之外的元素组成的一个列表。

TweetSet其中包含两个实现,一个是空的列表Nil,另外一个是非空列表Cons:

object Nil extends TweetList {
  def head = throw new java.util.NoSuchElementException("head of EmptyList")
  def tail = throw new java.util.NoSuchElementException("tail of EmptyList")
  def isEmpty = true
}

class Cons(val head: Tweet, val tail: TweetList) extends TweetList {
  def isEmpty = false
}

可以看到Nil的head和tail都是一个会抛出异常的元素访问,并且不可能出现多个空列表实例,这里Nil被定义成为一个单例对象,即Object。

Cons的元素就包含两个,一个是Tweet类对象的head,另外tail作为列表出现。

扯得有点远,回过头来说,如果从一个TweetSet开始,按照retweet大小(即影响的高低)将元素重新排列成为一个TweetList,那么就要从Nil开始,做以下几步动作:

1.每次挑选出retweet数量最大的Tweet节点

2.将挑选出来的节点加入到要返回的TweetList中

3.在原来的TweetSet中删除掉这个节点

这三个过程连接起来就组成了按照影响力排序这个函数,其函数签名如下:

def descendingByRetweet: TweetList

可以看到这个方法没有参数,返回值是一个TweetList对象。

从以上列出的3点出发,继续看题目,发现第3步已经有相应的方法实现了:

  def remove(tw: Tweet): TweetSet =
    if (tw.text < elem.text) new NonEmpty(elem, left.remove(tw), right)
    else if (elem.text < tw.text) new NonEmpty(elem, left, right.remove(tw))
    else left.union(right)

那我们只需要做第1、2步的工作就OK了。

首先看第一步,第一步需要完成的工作在TweetSet这个抽象类中也有给出原型:

def mostRetweeted: Tweet = ???

这里没有给出实现,因为这里不需要。但是我们需要在Empty和NonEmpty中分别实现这个方法,一个空集应该如何返回它里面最受欢迎的推特呢?我们可以使用特殊的字符来标记,比如返回使用-1表示retweet数量的Tweet对象。

NonEmpty中呢?按照之前的思路,当然是比较其对应的二叉树根元素和左右子树的mostRetweeted元素谁更受欢迎,然后返回最受欢迎的那个。定义三个Tweet对象,分别表示根元素、左子树retweet最大的元素也就是左子树的mostRetweet的结果以及右子树retweet最大的元素,然后通过比较返回retweet最大的那个元素。因为左右子树的左右子树的左右子树(有限次循环后)最终是一个空集,我们对空集的定义决定了这个递归最终会返回结果,并且得到的Tweet对象有最大的retweet数量。

第二部分的工作就是将得到的最流行的Tweet加入到列表中并返回,当然这里需要判断如果返回的Tweet的转发量为-1(我们前面定义的魔鬼数字)的话,就需要返回Nil,表示空集的排序结果仍然是空列表。否则需要返回最流行的Tweet为head,原TweetSet删除这个最流行的Tweet之外剩下的集合按照retweet数量排序得到的列表为tail的列表。求出tail这部分的描述是不是有点绕?简单来说就是将原有的TweetSet删除最流行的节点之后,再调用descendingByRetweet得到的列表。

看,又是一次漂亮的递归。

Tying everything together

以上完成了filter、union和排序功能,现在我们可以做一点小事情了。比如说在一堆推中选择被转发次数最多的一批。作业工程中的TweetReader.scala中包含了一批推特数据,作业中已经完成了对她们的汇总,通过调用TweetReader.allTweets可以返回这些推特的集合,使用一个TweetSet对象表示。

另外TweetSet.scala中给出了两个关键词列表:第一个列表包含一系列Google和Android相关的词汇,第二个列表包含一系列Apple和iOS相关的词汇。

作业首先要求完成对两个TweetSet的赋值,googleTweets包含所有text中提到第一个词汇列表中所有词汇的Tweet对象,appleTweets中包含所有text中提到第二个词汇列表中所有词汇的Tweet对象。这两个TweetSet对象都是lazy变量(关于这个变量的用法后续我们会介绍),其签名如下:

lazy val googleTweets: TweetSet
lazy val appleTweets: TweetSet

将这两个变量赋值之后,作业还要求将其并集中的元素按照retweet数量来排序,即完成以下函数体:

 /**
   * A list of all tweets mentioning a keyword from either apple or google,
   * sorted by the number of retweets.
   */
  lazy val trending: TweetList = ???

首先看这两个变量的赋值,当然需要首先定义一个变量来存储所有的推特,这个TweetSet对象的赋值就不多说了,一条语句就能搞定。

然后我们需要使用filter方法对推特全集进行过滤,过滤条件是什么呢?当然是是否包含相应列表中的词汇。所以filter的参数表示的判定条件就应该是是否至少包含一个列表中的词汇。

两个TweetSet的获取方法类似,只是传入的变量不同而已。

trending这个方法就更简单了,将得到的googleTweets和appleTweets做一次union操作,使用得到的并集调用排序方法即可。

结语

在这个作业中强调的地方有两点:1.注意这些类都是immutable的,也就是说对类对象做的操作不会修改原有对象的值,而是通过返回一个新对象的形式来体现,这也是函数式编程中一个很重要的概念即闭包(closure);2.由于TweetSet和TweetList的表示方式,很容易使用递归的方式来完成函数的作用,在递归过程中,不要忘记将上一步得到的结果作为下一步的参数传入,否则会出现错误的哦!

 

另外,还有两个地方值得大家细细品味:一个是除了需要完成的函数之外,题目默认给出了很多函数的实现方式,这些实现很符合函数式编程的理念,代码也写的很neat,推荐阅读并从中学习;另外一个是在test路径下有一个TweetSetSuite测试类,可以通过完善其中的测试用例来检验你所编写的函数的健壮性!

 

Week3 题目下载地址:http://download.csdn.net/detail/doggie_wangtao/7395957

以往的习题解析:

Coursera公开课Functional Programming Principles in Scala习题解答:Week 2

Coursera公开课Functional Programming Principles in Scala习题解答:Week 1

 

声明:

本文为原创,禁止用于任何商业用途,转载请注明出处:http://blog.csdn.net/asongoficeandfire/article/details/26842279