implicit.ly

Scala software, hot off the presses

« Back to blog

shapeless 1.0

Selected highlights of this 1.0 release of shapeless include,

  • A new encoding of polymorphic function values which optionally supports type specific cases, and which is interoperable with Scala's ordinary monomorphic function values.

```scala // choose is a function from Sets to Options with no type specific cases object choose extends (Set ~> Option) { def default[T](s : Set[T]) = s.headOption }

// choose is convertible to an ordinary monomorphic function value
val lo = List(Set(1, 3, 5), Set(2, 4, 6)) map choose
lo == List(Option(1), Option(2))

// size is a function from values of arbitrary type to a 'size' which is
// defined via type specific cases
object size extends (Id ~> Const[Int]#λ) {
  def default[T](t : T) = 1
}
implicit def sizeInt = size.λ[Int](x => 1)
implicit def sizeString = size.λ[String](s => s.length)
implicit def sizeList[T] = size.λ[List[T]](l => l.length)
implicit def sizeOption[T](implicit cases : size.λ[T]) =
  size.λ[Option[T]](t => 1+size(t.get))
implicit def sizeTuple[T, U](implicit st : size.λ[T], su : size.λ[U]) =
  size.λ[(T, U)](t => size(t._1)+size(t._2))

size(23) == 1
size("foo") == 3
size((23, "foo")) == 4

```

```scala val nested = List(Option(List(Option(List(Option(23))))))

val succ = everywhere(inc)(nested)
succ == List(Option(List(Option(List(Option(24))))))

```

  • A Typeable type class which provides a type safe cast operation.

```scala val a : Any = List(Vector("foo", "bar", "baz"), Vector("wibble"))

val lvs : Option[List[Vector[String]]] = a.cast[List[Vector[String]]]
lvs.isDefined == true

val lvi : Option[List[Vector[Int]]] = a.cast[List[Vector[Int]]]
lvi.isEmpty == true

```

  • The mother of all Scala HList's, which amongst other things,
  • is covariant.

```scala trait Fruit case class Apple() extends Fruit case class Pear() extends Fruit

type FFFF = Fruit :: Fruit :: Fruit :: Fruit :: HNil
type APAP = Apple :: Pear :: Apple :: Pear :: HNil

val a : Apple = Apple()
val p : Pear = Pear()

val apap : APAP = a :: p :: a :: p :: HNil
val ffff : FFFF = apap  // APAP <: FFFF

```

  • has a map operation, applying a polymorphic function value (possibly with type specific cases) across its elements. This means that it subsumes both typical HList's and also KList's (HList's whose elements share a common outer type constructor).

```scala type SISS = Set[Int] :: Set[String] :: HNil type OIOS = Option[Int] :: Option[String] :: HNil

val sets : SISS = Set(1) :: Set("foo") :: HNil
val opts : OIOS = sets map choose
opts == Option(1) :: Option("foo") :: HNil

```

  • has a zipper for traversal and persistent update.

```scala val l = 1 :: "foo" :: 3.0 :: HNil

val l2 = l.toZipper.right.put("wibble", 45).toHList
l2 == 1 :: ("wibble", 45) :: 3.0 :: HNil

val l3 = l.toZipper.right.delete.toHList
l3 == 1 :: 3.0 :: HNil

val l4 = l.toZipper.last.left.insert("bar").toHList
l4 == 1 :: "foo" :: "bar" :: 3.0 :: HNil, l5)

```

  • has a unify operation which converts it to an HList of elements of the least upper bound of the original types.

`scala val ffff = apap.unify // type inferred as FFFF `

  • supports conversion to an ordinary Scala List of elements of the least upper bound of the original types.

`scala val lf = apap.toList // type inferred as List[Fruit] lf == List(a, p, a, p) `

  • has a Typeable type class instance, allowing, eg. vanilla List[Any]'s or HList's with elements of type Any to be safely cast to precisely typed HList's.

```scala val ffff : FFFF = apap.unify // discard precise typing val apap2 : Option[APAP] = ffff.cast[APAP] // reestablish precise typing apap2.get == apap
```

These last three bullets make this HList dramatically more practically useful than HList's are typically thought to be: normally the full type information required to work with them is too fragile to cross subtyping or I/O boundaries. This implementation supports the discarding of precise information where necessary (eg. to serialize a precisely typed record after construction), and its later reconstruction (eg. a weakly typed deserialized record with a known schema can have it's precise typing reestabilished).

  • Conversions between tuples and HList's, and between ordinary Scala functions of arbitrary arity and functions which take a single corresponding HList argument. One application of this is the liftO function which lifts an ordinary function of arbitrary arity into Option.

```scala // Round trip from tuple to HList and back val t1 = (23, "foo", 2.0, true)

val l1 = t1.hlisted
h1 == 23 :: "foo" :: 2.0 :: true :: HNil

val t2 = l1.tupled
t1 == t2

// Lift these ordinary Scala function values into Option 
val sum : (Int, Int) => Int = _ + _
val prd : (Int, Int, Int) => Int = _ * _ * _

// Nb. liftO abstracts over the arity of its function arguments 
val sumO = liftO(sum) // (Option[Int], Option[Int]) => Option[Int]
val prdO = liftO(prd) // (Option[Int], Option[Int], Option[Int]) => Option[Int]

val s1 = sumO(Some(1), Some(2))
s1 == Option(3)

val s2 = sumO(Some(1), None)
s2 == None

val p1 = prdO(Some(2), Some(3), Some(4))
p1 == Option(24)

val p2 = prdO(Some(2), None, Some(4))
p2 == None

```

shapeless is an exploration of type class and dependent type based generic programming in Scala.

A series of articles on the implementation techniques used will appear here.