24

I'd like to link 2 columns of unique identifiers and be able to get a first column value by a second column value as well as a second column value by a first column value. Something like

Map(1 <-> "one", 2 <-> "two", 3 <-> "three")

Is there such a facility in Scala?

Actually I need even more: 3 columns to select any in a triplet by another in a triplet (individual values will never be met more than once in the entire map). But a 2-column bidirectional map can help too.

Ivan
  • 63,011
  • 101
  • 250
  • 382

6 Answers6

11

Guava has a bimap that you can use along with

import scala.collection.JavaConversions._
James Moore
  • 8,636
  • 5
  • 71
  • 90
Steve
  • 249
  • 1
  • 3
10

My BiMap approach:

object BiMap {
  private[BiMap] trait MethodDistinctor
  implicit object MethodDistinctor extends MethodDistinctor
}

case class BiMap[X, Y](map: Map[X, Y]) {
  def this(tuples: (X,Y)*) = this(tuples.toMap)
  private val reverseMap = map map (_.swap)
  require(map.size == reverseMap.size, "no 1 to 1 relation")
  def apply(x: X): Y = map(x)
  def apply(y: Y)(implicit d: BiMap.MethodDistinctor): X = reverseMap(y)
  val domain = map.keys
  val codomain = reverseMap.keys
}

val biMap = new BiMap(1 -> "A", 2 -> "B")
println(biMap(1)) // A
println(biMap("B")) // 2

Of course one can add syntax for <-> instead of ->.

Matthias Braun
  • 32,039
  • 22
  • 142
  • 171
Peter Schmitz
  • 5,824
  • 4
  • 26
  • 48
  • This map couldn't be extended? (I have to make new one, in order to add new key-value pairs) – om-nom-nom Mar 24 '12 at 12:19
  • @om-nom-nom You are referring to `mutable` versus `immutable` (this one)?! – Peter Schmitz Mar 24 '12 at 12:31
  • Yes, indeed, this is a drawback, if you want a mutable one. :D – Peter Schmitz Mar 24 '12 at 12:38
  • The only problem I could see is the memory footprint, because two maps are retained. Hopefully, it's easy to fix by using `private val reverseMap = map.view map (_.swap)`. My 2c – Andy Petrella Mar 24 '12 at 12:40
  • 2
    @andypetrella But that means the original map is swapped every time `apply(y: Y)` is called, so you have a runtime problem at `codomain`! – Peter Schmitz Mar 24 '12 at 12:45
  • One can make the `reverseMap` `lazy` but this only saves memory until first codomain access. By the way, I am just wondering what the additional memory footprint is! It´s independent of the size of objects in the BiMap, but dependent of the amount of objects, isn´t it? – Peter Schmitz Mar 24 '12 at 12:48
  • @peter-schmitz indeed... but codomain is just `map.values` actually. Just thinking – Andy Petrella Mar 24 '12 at 12:49
  • @om-nom-nom If you want a mutable one, please see the answer that I've posted. – Destin Mar 24 '12 at 16:40
  • 2
    Using double the memory for double Map capability seems like a fair tradeoff to me. – Dan Burton Mar 24 '12 at 20:53
  • 2
    This seems a lot like the guava ```BiHashMap``` in that it has two maps and they are immutable. But you seem to be using the type to do away with the guava ```.reverse``` method. Doesn't that make String <-> String impossible? In which case you'd add back the ```.reverse```? And wouldn't that do away with the ```methodDistinctor```? Which would give you exactly BiHashMap, right? – pferrel Apr 24 '14 at 21:42
3

Here's a quick Scala wrapper for Guava's BiMap.

import com.google.common.{collect => guava}
import scala.collection.JavaConversions._
import scala.collection.mutable
import scala.languageFeature.implicitConversions

class MutableBiMap[A, B] private (
    private val g: guava.BiMap[A, B] = new guava.HashBiMap[A, B]()) {

  def inverse: MutableBiMap[B, A] = new MutableBiMap[B, A](g.inverse)
}

object MutableBiMap {

  def empty[A, B]: MutableBiMap[A, B] = new MutableBiMap()

  implicit def toMap[A, B] (x: MutableBiMap[A, B]): mutable.Map[A,B] = x.g
}
Chris Martin
  • 30,334
  • 10
  • 78
  • 137
  • For scala 2.12 compatibility, use `guava.HashBiMap.create[A, B]()` near the top and at the end `= x.g.asScala` – Stefan L Dec 12 '16 at 15:00
2

I have a really simple BiMap in Scala:

  case class BiMap[A, B](elems: (A, B)*) {

    def groupBy[X, Y](pairs: Seq[(X, Y)]) = pairs groupBy {_._1} mapValues {_ map {_._2} toSet}

    val (left, right) = (groupBy(elems), groupBy(elems map {_.swap}))

    def apply(key: A) = left(key)
    def apply[C: ClassTag](key: B) = right(key)
  }

Usage:

  val biMap = BiMap(1 -> "x", 2 -> "y", 3 -> "x", 1 -> "y")
  assert(biMap(1) == Set("x", "y"))
  assert(biMap("x") == Set(1, 3))
pathikrit
  • 32,469
  • 37
  • 142
  • 221
  • Like this! 'groupBy' could be marked private. Why is the 'C: ClassTag' needed? Why make it a case class? – akauppi Apr 24 '15 at 13:26
  • The `C: ClassTag` hack is to make the scala compiler happily compile both the apply methods :) – pathikrit Apr 24 '15 at 16:55
  • 1
    @AndresF. If `A` and `B` are same, you just have to pass in a fake `C` e.g. `biMap(1)` would do left and `biMap[Int](1)` would do right – pathikrit Apr 24 '15 at 16:59
  • @akauppi: I wrote it as a case class so I didn't have to write `new BiMap` :) – pathikrit Apr 24 '15 at 17:01
  • Thought so. :) What's the purpose of the groupBy (I don't quite understand it)? I made my own copy that simply creates two internal Map's. – akauppi Apr 25 '15 at 18:53
  • The groupBy is for duplicates. If you simply call `elems.toMap`, you will only get the last one . In my example, `1` maps to both `x` and `y`. If you do `toMap`, it will only store the last one i.e. `y`. Depends on what you need. – pathikrit Apr 25 '15 at 19:51
  • Okay, thanks for explaining. I did an invariant check that doesn't allow duplicates. – akauppi Apr 27 '15 at 07:52
  • Yeah depends on what you need. I guess my example is more generic and should be called `BiMultiMap` – pathikrit Apr 27 '15 at 08:10
1

I don't think it exists out of the box, because the generic behavior is not easy to extract

How to handle values matching several keys in a clean api?

However for specific cases here is a good exercise that might help. It must be updated because no hash is used and getting a key or value is O(n).

But the idea is to let you write something similar to what you propose, but using Seq instead of Map...

With the help of implicit and trait, plus find, you could emulate what you need with a kind of clean api (fromKey, fromValue).

The specificities is that a value is not supposed to appear in several places... In this implementation at least.

  trait BiMapEntry[K, V] {
    def key:K
    def value:V
  }

  trait Sem[K] {

    def k:K

    def <->[V](v:V):BiMapEntry[K, V] = new BiMapEntry[K,  V]() { val key = k; val value = v}
  }

  trait BiMap[K, V] {

    def fromKey(k:K):Option[V]

    def fromValue(v:V):Option[K]
  }


  object BiMap {
    implicit def fromInt(i:Int):Sem[Int] = new Sem[Int] {
      def k = i
    }

    implicit def fromSeq[K, V](s:Seq[BiMapEntry[K, V]]) = new BiMap[K, V] {
      def fromKey(k:K):Option[V] = s.find(_.key == k).map(_.value)
      def fromValue(v:V):Option[K] = s.find(_.value == v).map(_.key)
    }

  }




  object test extends App {

    import BiMap._



    val a = 1 <-> "a"

    val s = Seq(1 <-> "a", 2 <-> "b")

    println(s.fromKey(2))
    println(s.fromValue("a"))

  }
om-nom-nom
  • 62,329
  • 13
  • 183
  • 228
Andy Petrella
  • 4,345
  • 26
  • 29
0

Scala is immutable and values are assigned as reference not copy, so memory footprint will for reference/pointer storage only, which it's better to use to two maps, with type A being key for first and type being B being key for second mapped to B and A respectively, than tun time swapping of maps. And the swapping implementation also has it's own memory footprint and the newly swapped hash-map will also be there in memory till the execution of parent call back and the garbage collector call. And if the the swapping of map is required frequently than virtually your are using equally or more memory than the naive two maps implementation at starting.

One more approach you can try with single map is this(will work only for getting key using mapped value):

def getKeyByValue[A,B](map: Map[A,B], value: B):Option[A] = hashMap.find((a:A,b:B) => b == value)

Code for Scala implementation of find by key:

/** Find entry with given key in table, null if not found.
   */
  @deprecatedOverriding("No sensible way to override findEntry as private findEntry0 is used in multiple places internally.", "2.11.0")
  protected def findEntry(key: A): Entry =
    findEntry0(key, index(elemHashCode(key)))

  private[this] def findEntry0(key: A, h: Int): Entry = {
    var e = table(h).asInstanceOf[Entry]
    while (e != null && !elemEquals(e.key, key)) e = e.next
    e
  }
Ravinder Payal
  • 2,884
  • 31
  • 40