1

Is it possible to swap the arguments of a constructor? Consider the following example:

case class Foo(a:Int, b:Int) {
  if (a > b) {
    val tmp = a
    a = b
    b = tmp
  }
}

The compiler throws an error because I reassign to val a at line 4 which is perfectly fine. However, I need immutable objects. Therefore, declaring a and b as variables is not an option.

Is there a known pattern how to solve this problem?

Max Maier
  • 985
  • 6
  • 16

4 Answers4

4

Make an inner swap method:

case class Foo(a: Int, b: Int) {
  def ifSwap = if (a > b) Foo(b, a) else this
}

val f1 = Foo(1,2).ifSwap // the result is Foo(1,2)
val f2 = Foo(2,1).ifSwap // the result is Foo(1,2)

If you want to preserve immutability then to change state you need either return new instance on each modification, or use some hardcore ways like Lenses, State, Records, etc... And as Prof. Odersky told on SD'13 talk, there are situations when you shouldn't be afraid of vars

4lex1v
  • 21,367
  • 6
  • 52
  • 86
  • 1
    You may also define a helper method on the companion object `def foo(a: Int, b: Int) = Foo(a, b).ifSwap` instead of repeating `.ifSwap` everywhere. – huynhjl Jul 18 '13 at 05:05
3

I suppose what you want to achieve that every instance of Foo has ordered its pair of values, is that right?

One possibility is not to make the class case and instead define its construction and extraction yourself. The class won't inherit from product, no pretty default toString etc., but otherwise it's usable just as a case class:

class Foo private (val a: Int, val b: Int);
object Foo {
  def apply(a: Int, b: Int): Foo =
    if (a < b)
      new Foo(a, b)
    else
      new Foo(b, a)

    def unapply(f: Foo): Option[(Int,Int)] = Some((f.a, f.b))
}

// test:
def printFoo(f: Foo) = f match {
  case Foo(x, y) => println(x + ", " + y);
}
printFoo(Foo(1,2))
printFoo(Foo(3,2))

See also:

Community
  • 1
  • 1
Petr
  • 62,528
  • 13
  • 153
  • 317
  • Turns out you can keep your *case* cake and eat it too. I mean keep your *case* class and still make the constructor private. See my answer. – huynhjl Jul 18 '13 at 05:01
2

It appears you can make the case class constructor private! Extending on @PetrPudlák answers, I make the constructor private and define a foo helper to create the case class objects:

case class Foo private (a: Int, b: Int)
object Foo {
  def foo(x: Int, y: Int) = if (x > y) Foo(y, x) else Foo(x, y)
}

Then I just use foo to instantiate the well formed Foo objects and the rest of the case class functionality works as expected (equality, hashcode, unapply):

import Foo._

foo(1, 2)                                 //> res0: worksheets.so.Foo = Foo(1,2)
foo(2, 1)                                 //> res1: worksheets.so.Foo = Foo(1,2)
foo(3, 4) == foo(4, 3)                    //> res2: Boolean = true
// Foo(4, 2) does not compile

// extractor/pattern matching works:
val Foo(a, b) = foo(10,1)                 //> a  : Int = 1
                                          //| b  : Int = 10

You could also name foo as something more meaningful like OrderedFoo or NormalFoo.

huynhjl
  • 41,520
  • 14
  • 105
  • 158
  • If the intention is for the case class Foo to reliably represent an ADT (Abstract Data Type) such that a is always assumed to be less than or equal to b, then it is possible to produce an "invalid" instance of this ADT via the following; "val foo1 = Foo(1, 2); val foo2 = foo1.copy(a = 10)" Also, due to case classes implementing java.io.Serializable, it is also possible to subvert valid values via a text editor and deserialization. I cover the steps to handle these issues in this answer on another question thread: http://stackoverflow.com/a/25538287/501113 – chaotic3quilibrium Aug 27 '14 at 23:41
1
scala> :paste
// Entering paste mode (ctrl-D to finish)

object Foo {
  def swapped(b: Int, a: Int) = Foo(a=a, b=b)
}
case class Foo(a: Int, b: Int)

// Exiting paste mode, now interpreting.

defined module Foo
defined class Foo

scala> Foo.swapped(1, 2) == Foo(2, 1)
res0: Boolean = true
Debilski
  • 66,976
  • 12
  • 110
  • 133