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:
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
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
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.
3.139.67.5