Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inner classes vs. immutability in Scala

Please look at the following toy example:

case class Person(name: String, address: Person#Address = null) {
  case class Address(street: String, city: String, state: String) {
    def prettyFormat = s"To $name of $city" // note I use name here
  }

  def setAddress(street: String, city: String, state: String): Person =
    copy(address=Address(street,city,state))

  def setName(n: String): Person = copy(name=n)
}

Do you see a bug there? Yes, the following code will print the same message (John) in both cases:

val p1 = Person("John").setAddress("Main", "Johntown", "NY")
println(p1.address.prettyFormat) // prints To John of Johntown
val p2 = p1.setName("Jane")
println(p2.address.prettyFormat) // prints To John of Johntown

Naturally this is because of the $outer reference in Address that is preserved in set methods, so the p2 inner object still refers to John. The issue could be fixed by the following or by recreation of the Address object (wouldn't it be nice if we had precooked copy-constructors in case classes?):

def setName(n: String) = copy(name=n).setAddress(address.street,address.city,address.state)

However, the problem becomes more annoying where there are several inner objects like this and tens of methods like setName. So my conclusion is that immutability and class-inner classes are mutually incompatible.

Question: is there a design pattern or a useful idiom for a structure of nested immutable objects in which inner objects need access to the outer objects to do their job.

So far I have considered passing person as implicit to prettyFormat or wrapping the inner methods into a Reader monad, so the current person will be applied to the monad returned by prettyFormat. Any other great ideas?

like image 253
Jacob Eckel Avatar asked Aug 31 '15 06:08

Jacob Eckel


People also ask

Are Scala classes immutable?

In Scala, all number types, strings, and tuples are immutable. The classes Point, Date, Student, and Card we defined above are all immutable. In other words, once a Point object has been created, its fields cannot be modified.

Why is immutability important in Scala?

Immutable objects and data structures are first-class citizens in Scala. This is because they prevent mistakes in distributed systems and provide thread-safe data.

What is mutable and immutable in Scala?

Scala collections systematically distinguish between mutable and immutable collections. A mutable collection can be updated or extended in place. This means you can change, add, or remove elements of a collection as a side effect. Immutable collections, by contrast, never change.

What is immutable in java?

An object is considered immutable if its state cannot change after it is constructed. Maximum reliance on immutable objects is widely accepted as a sound strategy for creating simple, reliable code. Immutable objects are particularly useful in concurrent applications.


2 Answers

It is not that immutability and class-inner classes are mutually incompatible, but when you create your inner address class, it is bind to that Person instance (else you would use a static inner class, i.e., define Address in a companion object).

You problem is more with the semantics of the copy method provided for case classes, which does not consider inner classes. So either you drop immutability or you create a REAL new Person on modification:

def setName(n: String): Person = Person(n, street, city, state)

Do note that I should not pass a direct Address instance to Person(), your very definition is that each Address type is part of a single person and only makes sense for that person, so it can't exist outside of that person, and I can't pass it from the outside to a new person being created. Again, if this is not the case, then you need to rethink you structure with different semantics.

Personally, I thing the following is much clearer/intuitive as a description of the domain:

case class Address(street: String, city: String, state: String)
case class Person(name: String, address: Address) {
   def prettyFormat = s"To $name of ${address.city}"
} 

And then you can create copies of addresses/people with little worry and full immutability.

like image 134
Daniel Langdon Avatar answered Sep 27 '22 22:09

Daniel Langdon


Use lenses to access nested immutable objects.

The boilerplate addressed by lenses already sets in with setAddress.

scala> case class Parent(name: String, child: Parent#Child) { case class Child(name: String) { def parent = Parent.this.name } }
defined class Parent

A would-be parent.

scala> val p1 = Parent("Bob", null)
p1: Parent = Parent(Bob,null)

scala> val p2 = Parent("Bob", new p1.Child("Billy"))
p2: Parent = Parent(Bob,Child(Billy))

Bob undergoes a change, but the child doesn't know her name.

scala> val p3 = p2.copy(name = "Mary")
p3: Parent = Parent(Mary,Child(Billy))

scala> p3.child.parent
res0: String = Bob

scala> import scalaz._
import scalaz._

scala> val parentName = Lens.lensu[Parent,String]((a,v)=>a.copy(name=v),_.name)
parentName: scalaz.Lens[Parent,String] = scalaz.LensFunctions$$anon$5@39bd45b4

Sample name transform.

scala> parentName =>= (_ + " Jo")
res1: Parent => Parent = <function1>

scala> res1(p1)
res3: Parent = Parent(Bob Jo,null)

scala> val parentChild = Lens.lensu[Parent, Parent#Child]((a,v)=>a.copy(child=a.Child(v.name)), _.child)
parentChild: scalaz.Lens[Parent,Parent#Child] = scalaz.LensFunctions$$anon$5@3cdeef1e

scala> val adopt = parentChild =>= identity
adopt: Parent => Parent = <function1>

If the parent name changes, the child must adopt it.

scala> val rename = res1 andThen adopt
rename: Parent => Parent = <function1>

scala> val p4 = rename(p3)
p4: Parent = Parent(Mary Jo,Child(Billy))

scala> p4.child.parent
res4: String = Mary Jo
like image 21
som-snytt Avatar answered Sep 27 '22 23:09

som-snytt