Ben Kovitz
Ben Kovitz

Reputation: 5020

How do you make a class flatMappable?

I'm finding myself wanting to write this:

def allAsAndFs(node: Node): PairOfSeqs[Action, Factor] =
  node.regularNames.flatMap { name => regularAsAndFs(name) } ++
  node.specialNames.flatMap { name => specialAsAndFs(name) }

where:

def regularNames: Seq[String]
def specialNames: Seq[String]

def regularAsAndFs(name: String): PairOfSeqs[Action, Factor]
def specialAsAndFs(name: String): PairOfSeqs[Action, Factor]

and:

class PairOfSeqs[A, B](as: Seq[A], bs: Seq[B]) {
  def ++(that: PairOfSeqs[A, B]): PairOfSeqs[A, B] =
    PairOfSeqs(as ++ that.as, bs ++ that.bs)
  . . .
}

What do I need to do to make flatMap coalesce the PairsOfSeqs by calling the appropriate ++ method?

It looks like PairOfSeqs should extend GenTraversableOnce, but GenTraversableOnce has a rather astonishing 47 abstract methods to override.

What's the idiomatic, Scalastic way to do this?

Upvotes: 2

Views: 171

Answers (1)

Rex Kerr
Rex Kerr

Reputation: 167901

You are using a somewhat nonstandard idea of flatMap. The normal type signature is

class C[A] {
  def flatMap[B](f: A => C[B]): C[B] = ???
}

But you want (something akin to--this will not compile):

  def foo[D <: { def ++(d: D): D }](f: A => D): D

which is much more like fold than flatMap.

One possibility is to make PairOfSeqs a full-fledged collection. That's going to be tough because the types really don't line up that well, and because extending collections is difficult. (The reward/effort ratio can be good because even though the effort is large the reward is huge, if you really need it.)

Perhaps the best possibility is just to have an extension method on Seq.

case class PairOfSeqs[A, B](as: Seq[A], bs: Seq[B]) {
  def ++(that: PairOfSeqs[A, B]): PairOfSeqs[A,B] =
    PairOfSeqs(as ++ that.as, bs ++ that.bs)
}

implicit class SeqCanPairMap[A](val underlying: Seq[A]) extends AnyVal {
  def flatPair[B,C](f: A => PairOfSeqs[B,C]): PairOfSeqs[B,C] = {
    val ps = underlying.map(f)
    PairOfSeqs(ps.map(_.as).fold(Seq[B]())(_ ++ _), ps.map(_.bs).fold(Seq[C]())(_ ++ _))
  }
}

Seq("fish", "fowl").flatPair(s => new PairOfSeqs(s.map(_.toChar), s.map(_.toInt)))

There are a variety of other options (e.g. define a more conventional flatMap on PairOfSeqs, and a conversion from regular seqs to a PairOfSeqs with an empty second slot), but this probably covers your use case well enough.

Finally, to make a class flatMappable in general, all you have to do is define flatMap. In particular, for-comprehensions can then use it. (But you'll need a map also or it won't be useful for for-comprehensions.)

Upvotes: 3

Related Questions