Map versus FlatMap on String

2019-01-13 14:06发布

问题:

Listening to the Collections lecture from Functional Programming Principles in Scala, I saw this example:

scala> val s = "Hello World"

scala> s.flatMap(c => ("." + c)) // prepend each element with a period
res5: String = .H.e.l.l.o. .W.o.r.l.d

Then, I was curious why Mr. Odersky didn't use a map here. But, when I tried map, I got a different result than I expected.

scala> s.map(c => ("." + c))
res8: scala.collection.immutable.IndexedSeq[String] = Vector(.H, .e, .l, .l, .o, 
                                                          ". ", .W, .o, .r, .l, 

I expected that above call to return a String, since I'm map-ing, i.e. applying a function to each item in the "sequence," and then returning a new "sequence."

However, I could perform a map rather than flatmap for a List[String]:

scala> val sList = s.toList
sList: List[Char] = List(H, e, l, l, o,  , W, o, r, l, d)

scala> sList.map(c => "." + c)
res9: List[String] = List(.H, .e, .l, .l, .o, ". ", .W, .o, .r, .l, .d)

Why was a IndexedSeq[String] the return type of calling map on the String?

回答1:

The reason for this behavior is that, in order to apply "map" to a String, Scala treats the string as a sequence of chars (IndexedSeq[String]). This is what you get as a result of the map invocation, where for each element of said sequence, the operation is applied. Since Scala treated the string as a sequence to apply map, that is what mapreturns.

flatMap then simply invokes flatten on that sequence afterwards, which then "converts" it back to a String



回答2:

You also have an interesting "collection of Scala flatMap examples", the first of which illustrates that difference between flatMap and map:

scala> val fruits = Seq("apple", "banana", "orange")
fruits: Seq[java.lang.String] = List(apple, banana, orange)

scala> fruits.map(_.toUpperCase)
res0: Seq[java.lang.String] = List(APPLE, BANANA, ORANGE)

scala> fruits.flatMap(_.toUpperCase)
res1: Seq[Char] = List(A, P, P, L, E, B, A, N, A, N, A, O, R, A, N, G, E)

Quite a difference, right?
Because flatMap treats a String as a sequence of Char, it flattens the resulting list of strings into a sequence of characters (Seq[Char]).
flatMap is a combination of map and flatten, so it first runs map on the sequence, then runs flatten, giving the result shown.

You can see this by running map and then flatten yourself:

scala> val mapResult = fruits.map(_.toUpperCase)
mapResult: Seq[String] = List(APPLE, BANANA, ORANGE)

scala> val flattenResult = mapResult.flatten
flattenResult: Seq[Char] = List(A, P, P, L, E, B, A, N, A, N, A, O, R, A, N, G, E)


回答3:

Your map function c => ("." + c) takes a char and returns a String. It's like taking a List and returning a List of Lists. flatMap flattens that back.

If you would return a char instead of a String you wouldn't need the result flattened, e.g. "abc".map(c => (c + 1).toChar) returns "bcd".



回答4:

With map you are taking a list of characters and turning it into a list of strings. That's the result you see. A map never changes the length of a list – the list of strings has as many elements as the original string has characters.

With flatMap you are taking a list of characters and turning it into a list of strings and then you mush those strings together into a single string again. flatMap is useful when you want to turn one element in a list into multiple elements, without creating a list of lists. (This of course also means that the resulting list can have any length, including 0 – this is not possible with map unless you start out with the empty list.)



回答5:

Use flatMap in situations where you run map followed by flattern. The specific situation is this:

• You’re using map (or a for/yield expression) to create a new collection from an existing collection.

• The resulting collection is a List of Lists.

• You call flatten immediately after map (or a for/yield expression).

When you’re in this situation, you can use flatMap instead.

Example: Add all the Integers from the bag

val bag = List("1", "2", "three", "4", "one hundred seventy five")

def toInt(in: String): Option[Int] = {
try {
Some(Integer.parseInt(in.trim))
} catch {
case e: Exception => None
}
}

Using a flatMap method

> bag.flatMap(toInt).sum

Using map method (3 steps needed)

bag.map(toInt) // List[Option[Int]] = List(Some(1), Some(2), None, Some(4), None)

bag.map(toInt).flatten //List[Int] = List(1, 2, 4)

bag.map(toInt).flatten.sum //Int = 7


标签: scala map