A while back this was asked and answered on the Scala mailing list:
Kevin:
Given some nested structure:
List[List[...List[T]]]
what’s the best (preferably type-safe) way to flatten it to aList[T]
Jesper:
A combination of implicits and default arguments works:
case class Flat[T, U](fn : T => List[U])
implicit def recFlattenFn[T, U](implicit f : Flat[T, U] = Flat((l : T)
=> List(l))) =
Flat((l : List[T]) => l.flatMap(f.fn))
def recFlatten[T, U](l : List[T])(implicit f : Flat[List[T], U]) = f.fn(l)
Examples:
scala> recFlatten(List(1, 2, 3))
res0: List[Int] = List(1, 2, 3)
scala> recFlatten(List(List(1, 2, 3), List(4, 5)))
res1: List[Int] = List(1, 2, 3, 4, 5)
scala> recFlatten(List(List(List(1, 2, 3), List(4, 5)), List(List(6, 7))))
res2: List[Int] = List(1, 2, 3, 4, 5, 6, 7)
I have been looking at this code for a while. I cannot figure out how it works. There seems to be some recursion involved… Can anybody shed some light? Are there other examples of this pattern and does it have a name?
Oh wow, this is an old one! I’ll start by cleaning up the code a bit and pulling it into line with current idiomatic conventions:
Then, without further ado, break down the code. First, we have our
Flatclass:This is nothing more than a named wrapper for the function
T => List[U], a function that will build aList[U]when given an instance of typeT. Note thatThere could also be aList[U], or aU, or aList[List[List[U]]], etc. Normally, such a function could be directly specified as the type of a parameter. But we’re going to be using this one in implicits, so the named wrapper avoids any risk of an implicit conflict.Then, working backwards from
recFlatten:This method will take
xs(aList[T]) and convert it to aU. To achieve this, it locates an implicit instance ofFlat[T,U]and invokes the enclosed function,fnThen, the real magic:
This satisfies the implicit parameter required by
recFlatten, it also takes another implicit paramater. Most crucially:recFlattenFncan act as its own implicit parameterrecFlattenFnwill only be implicitly resolved as aFlat[T,U]ifTis aListfcan fallback to a default value if implicit resolution fails (i.e.Tis NOT aList)Perhaps this is best understood in the context of one of the examples:
Tis inferred asList[List[Int]]recFlattenFnBroadly speaking:
Note that
recFlattenFnwill only match an implicit search for aFlat[List[X], X]and the type params[Int,_]fail this match becauseIntis not aList. This is what triggers the fallback to the default value.Type inference also works backwards up that structure, resolving the
Uparam at each level of recursion:Which is just a nesting of
Flatinstances, each one (except the innermost) performing aflatMapoperation to unroll one level of the nestedListstructure. The innermostFlatsimply wraps all the individual elements back up in a singleList.Q.E.D.