简体   繁体   中英

Scala: How to map a subset of a seq to a shorter seq

I am trying to map a subset of a sequence using another (shorter) sequence while preserving the elements that are not in the subset. A toy example below tries to give a flower to females only:

def giveFemalesFlowers(people: Seq[Person], flowers: Seq[Flower]): Seq[Person] = {
  require(people.count(_.isFemale) == flowers.length)
  magic(people, flowers)(_.isFemale)((p, f) => p.withFlower(f))
}

def magic(people: Seq[Person], flowers: Seq[Flower])(predicate: Person => Boolean)
         (mapping: (Person, Flower) => Person): Seq[Person] = ??? 

Is there an elegant way to implement the magic?

Use an iterator over flowers , consume one each time the predicate holds; the code would look like this,

val it = flowers.iterator
people.map ( p => if (predicate(p)) p.withFlowers(it.next) else p )

What about zip (aka zipWith) ?

scala> val people = List("m","m","m","f","f","m","f")
people: List[String] = List(m, m, m, f, f, m, f)

scala> val flowers = List("f1","f2","f3")
flowers: List[String] = List(f1, f2, f3)

scala> def comb(xs:List[String],ys:List[String]):List[String] = (xs,ys) match {
     |  case (x :: xs, y :: ys) if x=="f" => (x+y) :: comb(xs,ys)
     |  case (x :: xs,ys) => x :: comb(xs,ys)
     |  case (Nil,Nil) => Nil
     | }

scala> comb(people, flowers)
res1: List[String] = List(m, m, m, ff1, ff2, m, ff3)

If the order is not important, you can get this elegant code:

scala> val (men,women) = people.partition(_=="m")
men: List[String] = List(m, m, m, m)
women: List[String] = List(f, f, f)

scala> men ++ (women,flowers).zipped.map(_+_)
res2: List[String] = List(m, m, m, m, ff1, ff2, ff3)

I am going to presume you want to retain all the starting people (not simply filter out the females and lose the males), and in the original order, too.

Hmm, bit ugly, but what I came up with was:

def giveFemalesFlowers(people: Seq[Person], flowers: Seq[Flower]): Seq[Person] = {
    require(people.count(_.isFemale) == flowers.length)
    people.foldLeft((List[Person]() -> flowers)){ (acc, p) => p match {
        case pp: Person if pp.isFemale => ( (pp.withFlower(acc._2.head) :: acc._1) -> acc._2.tail)
        case pp: Person => ( (pp :: acc._1) -> acc._2)
    } }._1.reverse
}

Basically, a fold-left, initialising the 'accumulator' with a pair made up of an empty list of people and the full list of flowers, then cycling through the people passed in.

If the current person is female, pass it the head of the current list of flowers (field 2 of the 'accumulator'), then set the updated accumulator to be the updated person prepended to the (growing) list of processed people, and the tail of the (shrinking) list of flowers.

If male, just prepend to the list of processed people, leaving the flowers unchanged.

By the end of the fold, field 2 of the 'accumulator' (the flowers) should be an empty list, while field one holds all the people (with any females having each received their own flower), in reverse order, so finish with ._1.reverse

Edit : attempt to clarify the code (and substitute a test more akin to @elm's to replace the match , too) - hope that makes it clearer what is going on, @Felix! (and no, no offence taken):

def giveFemalesFlowers(people: Seq[Person], flowers: Seq[Flower]): Seq[Person] = {

    require(people.count(_.isFemale) == flowers.length)

    val start: (List[Person], Seq[Flower]) = (List[Person](), flowers)

    val result: (List[Person], Seq[Flower]) = people.foldLeft(start){ (acc, p) =>
        val (pList, fList) = acc
        if (p.isFemale) {
          (p.withFlower(fList.head) :: pList, fList.tail)
        } else {
          (p :: pList, fList)
        }
    }

    result._1.reverse
}

I'm obviously missing something but isn't it just

people map {
  case p if p.isFemale => p.withFlower(f)
  case p => p
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM