1 / 40

How to Fight Bit Rot With Types

How to Fight Bit Rot With Types. Martin Odersky AOSD 2010. Types Can Cause Bit Rot. Types make contracts explicit, so they should help keeping systems consistent. However, if type systems are too week, they might require unnecessary duplication, which can cause bit rot.

valiant
Download Presentation

How to Fight Bit Rot With Types

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. How to Fight Bit Rot With Types Martin Odersky AOSD 2010

  2. Types Can Cause Bit Rot Types make contracts explicit, so they should help keeping systems consistent. However, if type systems are too week, they might require unnecessary duplication, which can cause bit rot. Case Study: Scala collection framework.

  3. (quick dive into Scala

  4. Where it comes from Scala has established itself as one of the main alternative languages on the JVM. Prehistory: 1996 – 1997: Pizza 1998 – 2000: GJ, Java generics, javac Timeline: 2003 – 2006: The Scala Experiment 2006 – 2009: Driving industrial adoption

  5. What’s Scala?

  6. Scala is a Unifier Agile, with lightweight syntax Object-Oriented Scala Functional Safe and performant, with strong static typing

  7. Let’s see some code:

  8. A class ... publicclass Person { public final String name; public finalintage; Person(String name, int age) { this.name = name; this.age = age; } } ... in Java: class Person(val name: String, val age: Int) {} ... in Scala:

  9. ... and its usage import java.util.ArrayList; ... Person[] people; Person[] minors; Person[] adults; { ArrayList<Person> minorsList = new ArrayList<Person>(); ArrayList<Person> adultsList = new ArrayList<Person>(); for (int i = 0; i < people.length; i++) (people[i].age < 18 ? minorsList : adultsList) .add(people[i]); minors = minorsList.toArray(people); adults = adultsList.toArray(people); } ... in Java: A function value An infix method call A simple pattern match val people: Array[Person] val(minors, adults) = people partition (_.age < 18) ... in Scala:

  10. But there’s more to it

  11. Embedding Domain-Specific Languages Scala’s flexible syntax makes iteasy to define high-level APIs & embedded DSLs Examples: - Scala actors (the core of Twitter’s message queues) - specs, ScalaCheck - ScalaFX - ScalaQuery scalac’s plugin architecture makes it easy to typecheck DSLs and to enrich their semantics. // asynchronous message send actor!message // message receive receive { casemsgpat1 => action1 … casemsgpatn => actionn }

  12. The Bottom Line When going from Java to Scala, expect at least a factor of 2 reduction in LOC. But does it matter? Doesn’t Eclipse write these extra lines for me? This does matter. Eye-tracking experiments* show that for program comprehension, average time spent per word of source code is constant. So, roughly, half the code means half the time necessary to understand it. *G. Dubochet. Computer Code as a Medium for Human Communication: Are Programming Languages Improving? In 21st Annual Psychology of Programming Interest Group Conference, pages 174-187, Limerick, Ireland, 2009.

  13. How to get started 100s of resources on the web. Here are three great entry points: • Simply Scala • Scalazine @ artima.com • Scala for Javarefugees

  14. How to find out more Scala site: www.scala-lang.org Eight books last year

  15. )

  16. Some Scala Collections

  17. Collection Traits • object-oriented • generic:List[T], Map[K, V] • optionally persistent, e.g. collection.immutable.Seq • higher-order, with methods such as foreach, map, filter. • Uniform return type principle: Operations should return collections of the same type (constructor) as their left operand. scala> val ys = List(1, 2, 3) ys: List[Int] = List(1, 2, 3) scala> val xs: Seq[Int] = ys xs: Seq[Int] = List(1, 2, 3) scala> xs map (_ + 1) res0: Seq[Int] = List(2, 3, 4) scala> ys map (_ + 1) res1: List[Int] = List(2, 3, 4)

  18. Old Collection Structure traitIterable[A] { def filter(p: A => Boolean): Iterable[A] = ... def partition(p: A => Boolean) = (filter(p(_)), filter(!p(_))) def map[B](f: A => B): Iterable[B] = ... } traitSeq[A] extends Iterable[A] { def filter(p: A => Boolean): Seq[A] = ... overridedef partition(p: A => Boolean) = (filter(p(_)), filter(!p(_))) def map[B](f: A => B): Seq[B] = ... }

  19. Types force duplication filter needs to be re-defined on each level partition also needs to be re-implemented on each level, even though its definition is everywhere the same. The same pattern repeats for many other operations and types.

  20. Signs of Bit Rot Lots of duplications of methods. • Methods returning collections have to be repeated for every collection type. Inconsistencies. • Sometimes methods such as filter, map were not specialized in subclasses • More often, they only existed in subclasses, even though they could be generalized “Broken window” effect. • Classes that already had some ad-hoc methods became dumping grounds for lots more. • Classes that didn’t stayed clean.

  21. Excerpts from List.scala

  22. How to do better? Can we abstract out the return type? Look at map: Need to abstract out the type constructor, not just the type. But we can do that using Scala’s higher-kinded types! traitIterable[A] def map[B](f: A => B): Iterable[B] traitSeq[A] def map[B](f: A => B): Seq[B]

  23. HK Types Collection Structure trait TraversableLike[A, CC[X]] { def filter(p: A => Boolean): CC[A] def map[B](f: A => B): CC[B] } trait Traversable[A] extends TraversableLike[A, Traversable] trait Iterable[A] extends TraversableLike[A, Iterable] trait Seq[A] extends TraversableLike[A, Seq] Here, CC is a parameter representing a type constructor.

  24. Implementation with Builders All ops in Traversable are implemented in terms of foreach and newBuilder. trait Builder[A, Coll] { def += (elem: A) // add elems def result: Coll // return result } trait TraversableLike[A, CC[X]] { defforeach(f: A => Unit) defnewBuilder[B]: Builder[B, CC[B]] def map[B](f: A => B): CC[B] = { val b = newBuilder[B] foreach (x => b += f(x)) b.result } }

  25. Unfortunately ... ... things are not as parametric as it seems at first. Take: class BitSet extends Set[Int] scala> val bs = BitSet(1, 2, 3) bs: scala.collection.immutable.BitSet = BitSet(1, 2, 3) scala> bs map (_ + 1) res0: scala.collection.immutable.BitSet = BitSet(2, 3, 4) scala> bs map (_.toString + "!") res1: scala.collection.immutable.Set[java.lang.String] = Set(1!, 2!, 3!) Note that the result type is the “best possible” type that fits the element type of the new collection. Other examples: SortedSet, String.

  26. How to advance? HK-types: cannot be enforced all the way down the type hierarchy, so cannot be defined at the roots. Previous system: Leads to duplications and bit rot. We need more flexibility. Can we define our own type system for collections? Question: Given old collection type From, new element type Elem, and new collection type To: Can an operation on From build a collection of type To with Elem elements? Captured in: CanBuildFrom[From, Elem, To]

  27. Facts about CanBuildFrom Can be stated as axioms and inference rules: CanBuildFrom[Traversable[A], B, Traversable[B]] CanBuildFrom[Set[A], B, Set[B]] CanBuildFrom[BitSet, B, Set[B]] CanBuildFrom[BitSet, Int, BitSet] CanBuildFrom[String, Char, String] CanBuildFrom[String, B, Seq[B]] CanBuildFrom[SortedSet[A], B, SortedSet[B]] :- Ordering[B] where A and B are arbitrary types.

  28. Implicitly Injected Theories Type theories such as the one for CanBuildFrom can be injected using implicits. A predicate: trait CanBuildFrom[From, Elem, To] { def apply(coll: From): Builder[Elem, To] } Axioms: implicitdef bf1[A, B]: CanBuildFrom[Traversable[A], B, Traversable[B]] implicitdef bf2[A, B]: CanBuildFrom[Set[A], B, Set[B]] implicitdef bf3: CanBuildFrom[BitSet, Int, BitSet] Inference rule: implicitdef bf4[A, B] (implicit ord: Ordering[B]) : CanBuildFrom[SortedSet[A], B, SortedSet[B]]

  29. Connecting with Map • Here’s how map can be defined in terms CanBuildFrom: trait TraversableLike[A, Coll] { this: Coll => def foreach(f: A => Unit) def newBuilder: Builder[A, Coll] def map[B, To](f: A => B) (implicit cbf: CanBuildFrom[Coll, B, To]): To = { val b = cbf(this) foreach (x => b += f(x)) b.result } }

  30. So, no need for HK-Types? Yes, there is, in collection factories Factories provide common set of operations to build collections: empty, apply, fill, tabulate, iterate, ... scala> List(1, 2, 3) res84 List[Int] = List(1, 2, 3) scala> Vector(1, 2, 3) res5: Vector[Int] = Vector(1, 2, 3) scala> List.tabulate(2, 2, 2)(_ + _ + _) res6: List[List[List[Int]]] = List(List(List(0, 1), List(1, 2)), List(List(1, 2), List(2, 3))) scala> Vector.tabulate(2, 2, 2)(_ + _ + _) res7: Vector[Vector[Vector[Int]]] = Vector(Vector(Vector(0, 1), Vector(1, 2)), Vector(Vector(1, 2), Vector(2, 3)))

  31. Collection Factories Implementation in terms of higher-kinded types: trait TraversableFactory[CC[X] >: Null <: Traversable[X]] { def newBuilder[A]: CC[A] def apply[A](elems: A*): CC[A] = ... def tabulate[A](n1: Int)(f: Int => A): CC[A] = ... def tabulate[A](n1: Int, n2: Int)(f: (Int, Int) => A): CC[CC[A]] = ... def tabulate[A](n1: Int, n2: Int, n3: Int)(f: (Int, Int, Int) => A) : CC[CC[CC[A]]] = ... } object Traversable extends TraversableFactory[Traversable] { def newBuilder[A]: Traversable[A] = new ListBuffer[A] } Alternative implementation with CanBuildFrom is possible but painful.

  32. Resumé • Higher-kinded types work 95% of the time • Unfortunately for collection methods this is not enough,because they’d have to be defined in the root, to be inherited everywhere. • Another case of the fragile base class problem • On the other hand, HK types work great in factories, because a collection object can choose to inherit from a factory or not.

  33. Objections

  34. Type Views • How to explain def map[B, To](f: A => B) (implicit cbf: CanBuildFrom[Coll, B, To]): To to a beginner? • Key observation: We can approximate the type of map. • For everyone but the most expert user def map[B](f: A => B): Traversable[B] // in class Traversabledef map[B](f: A => B): Seq[B] // in class Seq, etc is detailed enough. • These types are correct, they are just not as general as the type that’s actually implemented.

  35. Possible Solution: Flexible Doc Comments

  36. Going Further? • Modern software demands increasingly sophisticated type systems. • correctness, performance, parallelism. • Tendency to cover more and more aspects of program execution: • interfaces, mutability, effects, ownership, thread-locality, regions, ... • Risk that programmers are left behind. • How to overcome this “type wall”? • Are better tools the right answer?

  37. Could Dynamic Typing Have Helped? trait Iterable[A] { def filter(p) = ... def partition(p) = (filter(p(_)), filter(!p(_))) def map[B](f) = ... } trait Seq[A] extends Iterable[A] { } Here Yes

  38. Could Dynamic Typing Have Helped? scala> val bs = BitSet(1, 2, 3) bs: BitSet = BitSet(1, 2, 3) scala> bs map (_ + 1) res0: BitSet = BitSet(2, 3, 4) scala> bs map (_.toString + "!") res1: Set[java.lang.String] = Set(1!, 2!, 3!) Here? Much Harder

  39. Thank You Questions?

More Related