Reputation: 4877
Suppose I have a list of elements myl
and a function f
. I would like to chop up myl
into a list of lists, where each new "sublist" contains a contiguous stretch of myl
on which the value of f
is constant.
For example, if myl = List( (1,2), (3,2), (4,1), (6,2) )
and def f(x: (Int, Int)) = x._2
. Then the result should be List( List((1,2), (3,2)), List((4, 1)), List((6,2)) )
.
Is there an elegant way to write such a function without any var
s?
Upvotes: 1
Views: 198
Reputation: 125119
def groupBy[A](as: List[A])(p: (A, A) => Boolean): List[List[A]] =
as.foldRight(List.empty[List[A]]) {
case (x, (ys @ y :: _) :: zs) if p(x, y) => (x :: ys) :: zs
case (x, ys) => List(x) :: ys
}
scala> groupBy(myl)(_._2 == _._2)
res0: List[List[(Int, Int)]] = List(List((1,2), (3,2)), List((4,1)), List((6,2)))
Edit: I also wrote this version, using span
:
def groupBy[A](as: List[A])(p: (A, A) => Boolean): List[List[A]] =
as match {
case x :: xs =>
val (ys, zs) = xs.span(p(x, _))
(x :: ys) :: groupBy(zs)(p)
case _ => Nil
}
This is essentially similar to ghik's solution, but using pattern matching instead of isEmpty
and head
.
(An explanation of the name groupBy
: there is a function of the same name in the Haskell library, which has this exact same behaviour.)
Upvotes: 4
Reputation: 43309
Your problem is too specific for there to exist a general function solving it, so we'll have to write a function of our own.
The standard strategy to implementing functional algorithms is Divide and conquer, which basically means extracting the smallest part of the problem and then gradually building up the algorithm over it.
Okay, the obviously smallest thing we'll need is to test two items for contiguity:
def testContiguity( a : (Int, Int), b : (Int, Int) ) : Boolean
= a._2 == b._2
Then we'll need some function arranging the lists using the two-item comparison function. Would be nice if the standard library had it, but it doesn't, so we define our own:
def arrange
[ A ]
( list : List[ A ] )
( f : (A, A) => Boolean )
: List[ List[ A ] ]
= list match {
case a :: b :: tail =>
if( f(a, b) ) putInFirstGroup( a, arrange( b :: tail )( f ) )
else putInNewGroup( a, arrange( b :: tail )( f ) )
case a :: Nil =>
putInNewGroup( a, Nil )
case Nil =>
Nil
}
Okay, you can see that the above implementation relies on two other functions, let's define them too:
def putInFirstGroup
[ A ]
( item : A, groups : List[ List[ A ] ] )
: List[ List[ A ] ]
= groups match {
case group :: tail =>
(item :: group) :: tail
case Nil =>
(item :: Nil) :: Nil
}
def putInNewGroup
[ A ]
( item : A, groups : List[ List[ A ] ] )
: List[ List[ A ] ]
= (item :: Nil) :: groups
That's it!
scala> arrange( List( (1,2), (3,2), (4, 1), (6,2) ) )( testContiguity )
res2: List[List[(Int, Int)]] = List(List((1,2), (3,2)), List((4,1)), List((6,2)))
You can now see that we've created a pretty flexible and general solution, working on lists of items of any type and allowing you to use any other testing function to arrange them. Also we've heavily utilized division of a complex algorithm to small easily understandable parts to solve this.
Upvotes: 1
Reputation: 10764
Little more general solution:
def partitionBy[A](list: List[A])(groupingFunc: A => Any): List[List[A]] =
if (list.nonEmpty) {
val firstGroupingValue = groupingFunc(list.head)
val (group, rest) = list.span(groupingFunc(_) == firstGroupingValue)
group :: partitionBy(rest)(groupingFunc)
} else Nil
Example usage:
scala> partitionBy(List((1,2),(3,2),(5,2),(1,1),(2,1),(3,2),(5,2)))(_._2)
res14: List[List[(Int, Int)]] = List(List((1,2), (3,2), (5,2)), List((1,1), (2,1)), List((3,2), (5,2)))
Upvotes: 2
Reputation: 12988
You could try foldRight
val result2 = l.foldRight(List[List[(Int, Int)]]()) {
(x, acc) =>
if (acc.isEmpty) {
List(x) :: Nil
} else if (acc.head.head._2 == x._2) {
(x :: acc.head) :: acc.tail
} else {
List(x) :: acc
}
}
Upvotes: 1