3

I'm trying to model a relationship which can be reversed. For example, the reverse of North might be South. The reverse of Left might be Right. I'd like to use a case class to represent my relationships. I found a similar solution that uses case Objects here, but it's not quite what I want, here.

Here's my non-functional code:

case class Relationship(name: String, opposite:Relationship)

def relationshipFactory(nameA:String, nameB:String): Relationship = {
  lazy val x:Relationship = Relationship(nameA, Relationship(nameB, x))
  x
}

val ns = relationshipFactory("North", "South")

ns // North

ns.opposite // South

ns.opposite.opposite // North

ns.opposite.opposite.opposite // South

Can this code be changed so that:

  • It dosen't crash
  • I can create these things on demand as pairs.
Salim Fadhley
  • 6,975
  • 14
  • 46
  • 83

2 Answers2

4

If you really want to build graphs of immutable objects with circular dependencies, you have to declare opposite as def, and (preferably) throw one more lazy val into the mix:

abstract class Relationship(val name: String) {
  def opposite: Relationship
}

object Relationship {

  /** Factory method */
  def apply(nameA: String, nameB: String): Relationship = {
    lazy val x: Relationship = new Relationship(nameA) {
      lazy val opposite = new Relationship(nameB) {
        def opposite = x
      }
    }

    x
  }

  /** Extractor */
  def unapply(r: Relationship): Option[(String, Relationship)] =
    Some((r.name, r.opposite))

}

val ns = Relationship("North", "South")

println(ns.name)
println(ns.opposite.name)
println(ns.opposite.opposite.name)
println(ns.opposite.opposite.opposite.name)

You can quickly convince yourself that nothing bad happens if you run a few million rounds on this circle of circular dependencies:

// just to demonstrate that it doesn't blow up in any way if you
// call it hundred million times:
// Should be "North"
println((1 to 100000000).foldLeft(ns)((r, _) => r.opposite).name)

It indeed prints "North". It doesn work with case classes, but you can always add your own extractors, so this works:

val Relationship(x, op) = ns
val Relationship(y, original) = op
println(s"Extracted x = $x y = $y")

It prints "North" and "South" for x and y.


However, the more obvious thing to do would be to just save both components of a relation, and add opposite as a method that constructs the opposite pair.

case class Rel(a: String, b: String) {
  def opposite: Rel = Rel(b, a)
}

Actually, this is already implemented in the standard library:

scala> val rel = ("North", "South")
rel: (String, String) = (North,South)

scala> rel.swap
res0: (String, String) = (South,North)
Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
2

you have cyclic dependencies, this won't work. One option is to do:

case class Relationship(name: String)

and have a setter to specify the opposite. The factory would then do:

def relationshipFactory(nameA:String, nameB:String): Relationship = {
  val x:Relationship = Relationship(nameA)
  val opposite = Relationship(nameB)

  x.setOpposite(opposite)
  opposite.setOpposite(x)
  x
}

another option:

case class Relationship(name: String) {
  lazy val opposite = Utils.computeOpposite(this)
}

and have the opposite logic on the Utils object

yet another option: probably you don't want several South instances, so you should use case objects or enums (more on that at http://pedrorijo.com/blog/scala-enums/)

Using enums you can use pattern matching to do that logic without no overhead

pedrorijo91
  • 7,635
  • 9
  • 44
  • 82
  • 1
    "you have cyclic dependencies, this won't work" Why not? It's absolutely possible to create complex graphs of objects with cyclic dependencies in purely functional way, without any mutable variables or setters. E.g. parsers built from parser combinators are wonderful examples: highly self-referential, can unfold to arbitrary depth (at least as deep as the stack allows it), do not need any explicit wiring with setters. – Andrey Tyukin Feb 25 '18 at 12:55