Skip to content
Updating immutable objects in generic contexts.
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
project
src
.gitignore
LICENSE
README.md
TEAM
build.sbt

README.md

Motivation

This library aims at facilitating programming with immutable objects. Scala provides case classes for this purpose, and they are a good solution for most situations. However, they sometimes involve writing a lot of boilerplate. This problem is specially exacerbated when programming inheritance hierarchies, since abstract setters need to be implemented as well. You can find a more elaborate motivation to the updatable package in our blog: http://blog.hablapps.com.

Automatic generation of implementation classes

The updatable package allows you to generate implementation classes automatically:

scala> import org.hablapps.updatable._
import org.hablapps.updatable._

scala> import scala.language.reflectiveCalls
import scala.language.reflectiveCalls

scala> trait Person{
     |   val name: String
     |   val age: Int
     |   val friends: Traversable[String]
     | }
defined trait Person

scala> implicit val Person = builder[Person]
Person: org.hablapps.updatable.Builder[Person]{ ... }

The builder construct is actually a macro that instantiate the Builder type class for the Person type. The Person builder includes a factory method apply which enables the instantiation of person objects. Instantiation is carried out through an anonymous class which overrides the toString and == operators conveniently.

scala> val socrates = Person("Socrates",20,Set()) 
socrates: Person = Person(name=Socrates,age=20)

scala> val plato = Person("Plato",10,Set("Socrates"))
plato: Person = Person(name=Plato,age=10,friends=Set("Socrates"))

scala> val anotherSocrates = Person("Socrates",20,Set())
anotherSocrates: Person = Person(name=Socrates,age=20)

scala> anotherSocrates == socrates
res1: Boolean = true

The factory method is equipped with default parameters, obtained through the Default type class. The companion object of this type class already provides instantiations for common Scala types. Of course, you can define default values for your own types.

scala> val defaultPerson = Person()
defaultPerson: Person = Person(name=,age=0)

scala> val wittgenstein = Person("Ludwig")
wittgenstein: Person = Person(name=Ludwig,age=0)

Currently, if no value is provided for some attribute and no default is available, a runtime exception is thrown when the factory is invoked.

Var-like updates

The generated builder also allows us to update immutable objects of the corresponding type through the updated method. This method, however, is not type-safe, so it's better to use the := operator which mimics the syntax of var updates and won't let you assign values of the wrong type. This operator is enabled through an implicit macro conversion.

scala> socrates.age := 65
res9: org.hablapps.updatable.Updatable[Person] = Person(name=Socrates,age=65)

scala> socrates.age := "65"
<console>:20: error: type mismatch;
 found   : String("65")
 required: Int
              socrates.age := "65"
                              ^

scala> socrates.friends := Set("Plato")
res11: org.hablapps.updatable.Updatable[Person] = Person(name=Socrates,age=50,friends=Set(Plato))

scala> socrates.friends := Set(Plato)
<console>:21: error: type mismatch;
 found   : Person
 required: String
              socrates.friends := Set(Plato)
                                      ^

Note that the results of these updates are not plain values of type Person, but objects of type Updatable[Person]. Updatable objects are just like plain objects, with the difference that they also wrap the builder used in its instantiation. Most of the time, however, you can safely omit this feature since there is an implicit conversion from updatable values to plain ones.

For multi-valued attributes, besides the := operator we can also employ the += and -= operators -- provided there is evidence for a Modifiable instance of the corresponding type constructor:

scala> socrates.friends += "Plato"
res13: org.hablapps.updatable.Updatable[Person] = Person(name=Socrates,age=50,friends=Set(Plato))

scala> (socrates.friends := Set("Plato")).friends -= "Plato"
res15: org.hablapps.updatable.Updatable[Person] = Person(name=Socrates,age=50)

Currently, modifiable instances for Option and any kind of Traversable are defined. Note that although existence of Modifiable instances are statically checked, a runtime exception will be thrown if no modifiable evidence was found. This is to facilitate the use of modifiables at generic contexts.

Updates in generic contexts

Let's extend the Person trait with new types:

scala> trait ComputerScientist extends Person{ 
     |   val designerOf: Option[String] 
     | }
defined trait ComputerScientist

scala> implicit val ComputerScientist = builder[ComputerScientist]
ComputerScientist: org.hablapps.updatable.Builder[ComputerScientist]{...}

scala> trait Philosopher extends Person{ 
     |   val skeptic: Option[Boolean] 
     | }
defined trait Philosopher

scala> implicit val Philosopher = builder[Philosopher]
Philosopher: org.hablapps.updatable.Builder[Philosopher]{ ... }

scala> Philosopher()
res16: Philosopher = Philosopher(name=,age=0)

scala> Philosopher(_idealist=Some(true))
res17: Philosopher = Philosopher(name=,age=0,idealist=true)

scala> ComputerScientist(_name="McCarthy",_designerOf=Some("lisp")) 
res18: ComputerScientist = ComputerScientist(name=McCarthy,age=0,designerOf=lisp)

Now, let's define a generic function to increment the age of any person:

scala> def incAge[P <: Person : Builder](p: P): P = 
     |   p.age := p.age + 1
incAge: [P <: Person](p: P)(implicit evidence$1: org.hablapps.updatable.Builder[P])P

scala> incAge(Person())
res20: Person = Person(name=,age=1)

scala> incAge(Philosopher())
res21: Philosopher = Philosopher(name=,age=1)

scala> incAge(ComputerScientist())
res22: ComputerScientist = ComputerScientist(name=,age=1)

Note that the := operator can also be used within generic contexts - if we pass evidence of the right builder. This can be achieved in two ways: either explicitly, as in the example above, or encapsulated in an updatable object, as in the following example:

scala> def incAge[P <: Person](p: Updatable[P]): P = 
     |   p.age := p.age + 1
incAge: [P <: Person](p: org.hablapps.updatable.Updatable[P])P

scala> incAge(Philosopher())
res23: Philosopher = Philosopher(name=,age=1)

What about if we want to override some attribute?

The computer scientist and philosopher types shown above were simply defined by extending the Person type with additional fields. But we may also want to refine some existing field, such as the friends attribute. For instance, we may want to declare that philosophers store their friends in Sets and computer scientists in Lists. Let's try it and see what happens:

scala> trait ComputerScientist extends Person{ 
     |   val friends: List[String] 
     | }
defined trait ComputerScientist

scala> implicit val ComputerScientist = builder[ComputerScientist]
ERROR: attribute `friends` of type `Person` is overridden; can't generate builder

Ooops! As soon as we attempt to generate a builder for the ComputerScientist type, we are told that we can't do it because that type overrides an attribute. Why did we ban overriding? Because generic updates of overridable attributes are not type-safe.

But then, how do we refine some attribute? Well, we can follow the same strategy that we would if we didn't have the updatable package, and did have to implement the setters ourselves: we can employ auxiliary type members. For instance, we can abstract over the type constructor of the friends attribute using a FriendsCol[_] abstract type, as follows:

scala> :paste
trait Person {
  val name: String
  val age: Int

  type FriendsCol[_]
  val friends: FriendsCol[String]
}

implicit val Person = weakBuilder[Person]

trait Philosopher extends Person {
  type FriendsCol[x] = Set[x]
}

implicit val Philosopher = builder[Philosopher]

trait ComputerScientist extends Person {
  type FriendsCol[x] = List[x]
}

implicit val ComputerScientist = builder[ComputerScientist]

Note that the Person trait defined above can't be instantiated now without first defining the FriendsCol[_] type member, so we can't generate a builder for this type. Instead, we must generate a WeakBuilder which simply provides attribute reifications. If we tried to generate a builder, a warning would be issued:

<console>:20: warning: 
	Person is an abstract type. Are you sure you are not willing to use a 'weakBuilder[Person]' instead?.
	- Abstract attributes: friends
	- Abstract types: FriendsCol
	
         implicit val Person = builder[Person]

Now, if we want to define a new generic method over Person objects, we can refer to the actual declaration of the friends attribute using dependent types. For instance:

def setFriends[P <: Person : Builder](p: P)(persons: p.FriendsCol[String]): P =
  p.friends := persons

scala> setFriends(Philosopher())(List("Socrates"))
<console>:20: error: type mismatch;
 found   : List[String]
 required: scala.collection.immutable.Set[String]
              setFriends(Philosopher())(List("Socrates"))
                                            ^

Current & future work

Current work mostly focuses on fixing problems. But we would like to add more functionality in the near future. Here are some possible topics.

Lense-like composition

The library is certainly related to lenses, although our primary use case is not lense composition. It should be easy, though, to extend the library to support nested updates,

scala> trait T1{ val a1: T2 }; trait T2{ val a2: T3 }; trait T3{ val a3: Int }
scala> implicit val T1 = builder[T1]; ... 
scala> val t : T1 = T1(T2(T3(1))
...
scala> (t.a1.a2.a3 := v): T1
...

Parameterized factories

The idea is to be able to instantiate objects of traits that have type members undefined.

scala> trait T{ 
     |   type A1
     |   type A2
     |   val a1: A1
     |   val a2: A2 
     | }
scala>  val t: T = T[Int,String](1,"")

Type macros

Type macros fit our problem quite naturally. Eventually, we will create a branch to test the updatable package with the macro-paradise branch.

Using Updatable

Please, note that the library is currently in experimental status.

Updatable is published to the Hablapps Repository. Release and snapshot builds are published relative to Scala 2.10.2.

To build with Scala 2.10.2 add the following to your SBT (0.12.0 or later) configuration,

scalaVersion := "2.10.2"

scalaBinaryVersion <<= scalaVersion { sv => sv }

resolvers ++= Seq(
  "Hablapps - releases" at "http://repo.hablapps.org/releases",
  "Hablapps - snapshots" at "http://repo.hablapps.org/snapshots"
)

libraryDependencies ++= Seq(
  "org.hbalapps" % "updatable" %% "0.7.1"
)

(*) We have detected some problems with 2.10.1-RCX versions. We aim to fix this issue in the next weeks.

License

Updatable is released under Apache License, Version 2.0.

You can’t perform that action at this time.