Enumeratees

Enumeratee is also defined using a trait and its companion object with the same Enumeratee name.

It is defined as follows:

trait Enumeratee[From, To] {
...
def applyOn[A](inner: Iteratee[To, A]): Iteratee[From, Iteratee[To, A]]

def apply[A](inner: Iteratee[To, A]): Iteratee[From, Iteratee[To, A]] = applyOn[A](inner)
...
}

An Enumeratee transforms the Iteratee given to it as input and returns a new Iteratee. Let's look at a method that defines an Enumeratee by implementing the applyOn method. An Enumeratee's flatten method accepts Future[Enumeratee] and returns an another Enumeratee, which is defined as follows:

  def flatten[From, To](futureOfEnumeratee: Future[Enumeratee[From, To]]) = new Enumeratee[From, To] {
    def applyOn[A](it: Iteratee[To, A]): Iteratee[From, Iteratee[To, A]] =
      Iteratee.flatten(futureOfEnumeratee.map(_.applyOn[A](it))(dec))
  }

In the preceding snippet, applyOn is called on the Enumeratee whose future is passed and dec is defaultExecutionContext.

Defining an Enumeratee using the companion object is a lot simpler. The companion object has a lot of methods to deal with Enumeratees, such as map, transform, collect, take, filter, and so on. The API is documented at https://www.playframework.com/documentation/2.3.x/api/scala/index.html#play.api.libs.iteratee.Enumeratee$.

Let's define an Enumeratee by working through a problem. The example we used in the previous section to find the count of vowels and consonants will not work correctly if a vowel is capitalized in a sentence, that is, the result of src |>>> vowelCount will be incorrect when the line variable is defined as follows:

val line: String = "What we need is not the will to believe, but the wish to find out.".toUpperCase

To fix this, let's alter the case of all the characters in the data stream to lowercase. We can use an Enumeratee to update the input provided to the Iteratee.

Now, let's define an Enumeratee to return a given string in lowercase:

  val toSmallCase: Enumeratee[String, String] = Enumeratee.map[String] {
    s => s.toLowerCase
  }

There are two ways to add an Enumeratee to the dataflow. It can be bound to the following:

  • Enumerators
  • Iteratees

Binding an Enumeratee to an Enumerator

An Enumeratee can be bound to an enumerator by using the enumerator's through method, which returns a new Enumerator and is composed using the given enumeratee.

Updating the example to include an Enumeratee, we get this:

val line: String = "What we need is not the will to believe, but the wish to find out.".toUpperCase
val words: Seq[String] = line.split(" ")

val src: Enumerator[String] = Enumerator(words: _*)

private val vowels: Seq[Char] = Seq('a', 'e', 'i', 'o', 'u')
def getVowels(str: String): String = {
  val result = str.filter(c => vowels.contains(c))
  result
}

src.through(toSmallCase) |>>> vowelCount

The through method is an alias for the &> method and is defined for an enumerator, so the last statement can also be rewritten as follows:

src &> toSmallCase |>>> vowelCount

Binding an Enumeratee to an Iteratee

Now, let's implement the same flow by binding the enumeratee to the Iteratee. This can be done using the enumeratee's transform method. The transform method transforms the given Iteratee and results in a new Iteratee. Modifying the flow according to this, we get the following:

src |>>> toSmallCase.transform(vowelCount)

The enumeratee's transform method has a &>> symbolic alias. Using this, we can rewrite the flow as follows:

src |>>> toSmallCase &>> vowelCount

In addition to the fact that enumeratees can be bound to either Enumerators or Iteratees, different Enumeratees can be also be combined if the output type of one is the same as the input type of the other. For example, assume we have a filterVowel Enumeratee that filters out the vowels, as demonstrated in the following code:

val filterVowel: Enumeratee[String, String] = Enumeratee.map[String] {
  str => str.filter(c => vowels.contains(c))
}

Combining toSmallCase and filterVowel is possible since the output type of toSmallCase is a String and the input type of filterVowel is also a String. To do this, we use the Enumeratee's compose method:

toSmallCase.compose(filterVowel)

Now, let's rewrite the flow by using this:

src |>>> toSmallCase.compose(filterVowel) &>> sink

Here, sink is defined as follows:

  val sink: Iteratee[String, Int] = Iteratee.fold[String, Int](0)((x, y) => x + y.length)

Like the transform and compose methods, this also has a ><> symbolic alias. Let's define the flow using all the symbols instead of method names in the following way:

src |>>> toSmallCase ><> filterVowel &>> sink

We can add another enumeratee that computes the length of String and uses Iteratee, which simply sums up the lengths:

  val toInt: Enumeratee[String, Int] = Enumeratee.map[String] {
    str => str.length
  }
  val sum: Iteratee[Int, Int] = Iteratee.fold[Int, Int](0)((x, y) => x + y)
  src |>>> toSmallCase ><> filterVowel ><> toInt &>> sum
Binding an Enumeratee to an Iteratee

In the preceding snippet, we had to use a different iterator that accepts data of the Int type, since our toInt enumeratee transforms the String input to Int.

This concludes the chapter. Define a few data flows to get familiar with the API. Start with simpler data flows, such as extracting all the numbers or words in a given paragraph, and then complicate them gradually.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.69.199