-
Notifications
You must be signed in to change notification settings - Fork 533
Feature overview: shapeless 2.0.0
- Polymorphic function values
- Heterogenous lists
- HList-style operations on standard Scala tuples
- Facilities for abstracting over arity
- Heterogenous maps
- Singleton-typed literals
- Singleton-typed Symbols
- Extensible records
- Coproducts and discriminated unions
- Generic representation of (sealed families of) case classes
- Boilerplate-free lenses for arbitrary case classes
- Automatic type class instance derivation
- First class lazy values tie implicit recursive knots
- Collections with statically known sizes
- Type safe cast
- Testing for non-compilation
All the examples below assume you have previously imported shapeless.
Ordinary Scala function values are monomorphic. shapeless, however, provides an encoding of polymorphic function values. It supports natural transformations, which are familiar from libraries like Scalaz,
import poly._
// choose is a function from Sets to Options with no type specific cases
object choose extends (Set ~> Option) {
def apply[T](s : Set[T]) = s.headOption
}
scala> choose(Set(1, 2, 3))
res0: Option[Int] = Some(1)
scala> choose(Set('a', 'b', 'c'))
res1: Option[Char] = Some(a)
Being polymorphic, they may be passed as arguments to functions or methods and then applied to values of different types within those functions,
scala> def pairApply(f: Set ~> Option) = (f(Set(1, 2, 3)), f(Set('a', 'b', 'c')))
pairApply: (f: shapeless.poly.~>[Set,Option])(Option[Int], Option[Char])
scala> pairApply(choose)
res2: (Option[Int], Option[Char]) = (Some(1),Some(a))
They are nevertheless interoperable with ordinary monomorphic function values,
// choose is convertible to an ordinary monomorphic function value and can be
// mapped across an ordinary Scala List
scala> List(Set(1, 3, 5), Set(2, 4, 6)) map choose
res3: List[Option[Int]] = List(Some(1), Some(2))
However, they are more general than natural transformations and are able to capture type-specific cases which, as we'll see below, makes them ideal for generic programming,
// size is a function from Ints or Strings or pairs to a 'size' defined
// by type specific cases
object size extends Poly1 {
implicit def caseInt = at[Int](x => 1)
implicit def caseString = at[String](_.length)
implicit def caseTuple[T, U]
(implicit st : Case.Aux[T, Int], su : Case.Aux[U, Int]) =
at[(T, U)](t => size(t._1)+size(t._2))
}
scala> size(23)
res4: Int = 1
scala> size("foo")
res5: Int = 3
scala> size((23, "foo"))
res6: Int = 4
scala> size(((23, "foo"), 13))
res7: Int = 5
shapeless provides a comprehensive Scala HList
which has many features not shared by other HList implementations.
It has a map
operation, applying a polymorphic function value 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).
import poly._
// The same definition of choose as above
object choose extends (Set ~> Option) {
def apply[T](s : Set[T]) = s.headOption
}
scala> val sets = Set(1) :: Set("foo") :: HNil
sets: Set[Int] :: Set[String] :: HNil = Set(1) :: Set(foo) :: HNil
scala> val opts = sets map choose // map selects cases of choose for each HList element
opts: Option[Int] :: Option[String] :: HNil = Some(1) :: Some(foo) :: HNil
It also has a flatMap
operation,
import poly.identity
scala> val l = (23 :: "foo" :: HNil) :: HNil :: (true :: HNil) :: HNil
l: ((Int :: String :: HNil) :: HNil :: (Boolean :: HNil) :: HNil
= (23 :: foo :: HNil) :: HNil :: (true :: HNil) :: HNil
scala> l flatMap identity
res0: Int :: String :: Boolean :: HNil = 23 :: foo :: true :: HNil
It has a set of fully polymorphic fold operations which take a polymorphic binary function value. The fold is sensitive
to the static types of all of the elements of the HList
. Given the earlier definition of size,
object addSize extends Poly2 {
implicit def default[T](implicit st: size.Case.Aux[T, Int]) =
at[Int, T]{ (acc, t) => acc+size(t) }
}
scala> val l = 23 :: "foo" :: (13, "wibble") :: HNil
l: Int :: String :: (Int, String) :: HNil = 23 :: foo :: (13,wibble) :: HNil
scala> l.foldLeft(0)(addSize)
res1: Int = 11
It also has a zipper for traversal and persistent update,
import syntax.zipper._
scala> val l = 1 :: "foo" :: 3.0 :: HNil
l: Int :: String :: Double :: HNil = 1 :: foo :: 3.0 :: HNil
scala> l.toZipper.right.put(("wibble", 45)).reify
res0: Int :: (String, Int) :: Double :: HNil = 1 :: (wibble,45) :: 3.0 :: HNil
scala> l.toZipper.right.delete.reify
res1: Int :: Double :: HNil = 1 :: 3.0 :: HNil
scala> l.toZipper.last.left.insert("bar").reify
res2: Int :: String :: String :: Double :: HNil = 1 :: foo :: bar :: 3.0 :: HNil
It is covariant,
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
And it has a unify
operation which converts it to an HList
of elements of the least upper bound of the original
types,
scala> apap.unify
res0: Fruit :: Fruit :: Fruit :: Fruit :: HNil = Apple() :: Pear() :: Apple() :: Pear() :: HNil
It supports conversion to an ordinary Scala List
of elements of the least upper bound of the original types,
scala> apap.toList
res0: List[Fruit] = List(Apple(), Pear(), Apple(), Pear())
And it has a Typeable
type class instance (see below), allowing, eg. vanilla List[Any]
's or HList
's with elements
of type Any
to be safely cast to precisely typed HList
's.
import syntax.typeable._
scala> val ffff : FFFF = apap.unify
ffff: FFFF = Apple() :: Pear() :: Apple() :: Pear() :: HNil
scala> val precise: Option[APAP] = ffff.cast[APAP]
precise: Option[APAP] = Some(Apple() :: Pear() :: Apple() :: Pear() :: HNil)
These last three features 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).
shapeless allows standard Scala tuples to be manipulated in exactly the same ways as HList
s,
import syntax.std.tuple._
// head, tail, take, drop, split
scala> (23, "foo", true).head
res0: Int = 23
scala> (23, "foo", true).tail
res1: (String, Boolean) = (foo,true)
scala> (23, "foo", true).drop(2)
res2: (Boolean,) = (true,)
scala> (23, "foo", true).take(2)
res3: (Int, String) = (23,foo)
scala> (23, "foo", true).split(1)
res4: ((Int,), (String, Boolean)) = ((23,),(foo,true))
// prepend, append, concatenate
scala> 23 +: ("foo", true)
res5: (Int, String, Boolean) = (23,foo,true)
scala> (23, "foo") :+ true
res6: (Int, String, Boolean) = (23,foo,true)
scala> (23, "foo") ++ (true, 2.0)
res7: (Int, String, Boolean, Double) = (23,foo,true,2.0)
// map, flatMap
import poly._
object option extends (Id ~> Option) {
def apply[T](t: T) = Option(t)
}
scala> (23, "foo", true) map option
res8: (Option[Int], Option[String], Option[Boolean]) = (Some(23),Some(foo),Some(true))
scala> ((23, "foo"), (), (true, 2.0)) flatMap identity
res9: (Int, String, Boolean, Double) = (23,foo,true,2.0)
// fold (using previous definition of addSize)
scala> (23, "foo", (13, "wibble")).foldLeft(0)(addSize)
res10: Int = 11
// conversion to `HList`s and ordinary Scala `List`s
scala> (23, "foo", true).productElements
res11: Int :: String :: Boolean :: HNil = 23 :: foo :: true :: HNil
scala> (23, "foo", true).toList
res12: List[Any] = List(23, foo, true)
// zipper
import syntax.zipper._
scala> (23, ("foo", true), 2.0).toZipper.right.down.put("bar").root.reify
res13: (Int, (String, Boolean), Double) = (23,(bar,true),2.0)
Conversions between tuples and HList
's, and between ordinary Scala functions of arbitrary arity and functions which
take a single corresponding HList
argument allow higher order functions to abstract over the arity of the functions
and values they are passed,
import syntax.std.function._
import ops.function._
def applyProduct[P <: Product, F, L <: HList, R](p: P)(f: F)
(implicit gen: Generic.Aux[P, L], fp: FnToProduct.Aux[F, L => R]) =
f.toProduct(gen.to(p))
scala> applyProduct(1, 2)((_: Int)+(_: Int))
res0: Int = 3
scala> applyProduct(1, 2, 3)((_: Int)*(_: Int)*(_: Int))
res1: Int = 6
Shapeless provides a heterogenous map which supports an arbitrary relation between the key type and the corresponding value type,
// Key/value relation to be enforced: Strings map to Ints and vice versa
class BiMapIS[K, V]
implicit val intToString = new BiMapIS[Int, String]
implicit val stringToInt = new BiMapIS[String, Int]
val hm = HMap[BiMapIS](23 -> "foo", "bar" -> 13)
//val hm2 = HMap[BiMapIS](23 -> "foo", 23 -> 13) // Does not compile
scala> hm.get(23)
res0: Option[String] = Some(foo)
scala> hm.get("bar")
res1: Option[Int] = Some(13)
And in much the same way that an ordinary monomorphic Scala map can be viewed as a monomorphic function value, so too can a heterogenous shapeless map be viewed as a polymorphic function value,
scala> import hm._
import hm._
scala> val l = 23 :: "bar" :: HNil
l: Int :: String :: HNil = 23 :: bar :: HNil
scala> l map hm
res2: String :: Int :: HNil = foo :: 13 :: HNil
Although Scala's typechecker has always represented singleton types for literal values internally, there has not previously been syntax available to express them, other than by modifying the compiler. shapeless adds support for singleton-typed literals via implicit macros.
Singleton types bridge the gap between the value level and the type level and hence allow the exploration in Scala of
techniques which would typically only be available in languages with support for full-spectrum dependent types. The
latest iteration of shapeless records (see next bullet) makes a start on that. Another simpler application is the use of
Int
literals to index into HList
s and tuples,
import syntax.std.tuple._
scala> val l = 23 :: "foo" :: true :: HNil
l: Int :: String :: Boolean :: HNil = 23 :: foo :: true :: HNil
scala> l(1)
res0: String = foo
scala> val t = (23, "foo", true)
t: (Int, String, Boolean) = (23,foo,true)
scala> t(1)
res1: String = foo
The examples in the tests and the following illustrate other possibilities,
scala> import shapeless._, syntax.singleton._
import shapeless._
import syntax.singleton._
scala> 23.narrow
res0: Int(23) = 23
scala> "foo".narrow
res1: String("foo") = foo
scala> val (wTrue, wFalse) = (Witness(true), Witness(false))
wTrue: shapeless.Witness{type T = Boolean(true)} = $1$$1@212b9eca
wFalse: shapeless.Witness{type T = Boolean(false)} = $2$$1@36c5f0c9
scala> type True = wTrue.T
defined type alias True
scala> type False = wFalse.T
defined type alias False
scala> trait Select[B] { type Out }
defined trait Select
scala> implicit val selInt = new Select[True] { type Out = Int }
selInt: Select[True]{type Out = Int} = $anon$1@2c7b5e2a
scala> implicit val selString = new Select[False] { type Out = String }
selString: Select[False]{type Out = String} = $anon$2@57632e36
scala> def select(b: WitnessWith[Select])(t: b.instance.Out) = t
select: (b: shapeless.WitnessWith[Select])(t: b.instance.Out)b.instance.Out
scala> select(true)(23)
res2: Int = 23
scala> select(true)("foo")
<console>:18: error: type mismatch;
found : String("foo")
required: Int
select(true)("foo")
^
scala> select(false)(23)
<console>:18: error: type mismatch;
found : Int(23)
required: String
select(false)(23)
^
scala> select(false)("foo")
res3: String = foo
Scala's Symbol
type, despite having its own syntax and being isomorphic to the String
type, isn't equipped with
useful singleton-typed literals. An encoding of singleton types for Symbol
literals has proven to valuable (see
below), and is represented by tagging the non-singleton type with the singleton type of the corresponding String
literal,
scala> import syntax.singleton._
import syntax.singleton._
scala> 'foo // non-singleton type
res0: Symbol = 'foo
scala> 'foo.narrow // singleton type
res1: Symbol with shapeless.tag.Tagged[String("foo")] = 'foo
shapeless provides an implementation of extensible records modelled as HLists
of values tagged with the singleton
types of their keys. This means that there is no concrete representation needed at all for the keys. Amongst other
things this will allow subsequent work on Generic
to map case classes directly to records with their member names
encoded in their element types.
import shapeless._ ; import syntax.singleton._ ; import record._
val book =
("author" ->> "Benjamin Pierce") ::
("title" ->> "Types and Programming Languages") ::
("id" ->> 262162091) ::
("price" ->> 44.11) ::
HNil
scala> book("author") // Note result type ...
res0: String = Benjamin Pierce
scala> book("title") // Note result type ...
res1: String = Types and Programming Languages
scala> book("id") // Note result type ...
res2: Int = 262162091
scala> book("price") // Note result type ...
res3: Double = 44.11
scala> book.keys // Keys are materialized from singleton types encoded in value type
res4: String("author") :: String("title") :: String("id") :: String("price") :: HNil =
author :: title :: id :: price :: HNil
scala> book.values
res5: String :: String :: Int :: Double :: HNil =
Benjamin Pierce :: Types and Programming Languages :: 262162091 :: 44.11 :: HNil
scala> val newPrice = book("price")+2.0
newPrice: Double = 46.11
scala> val updated = book +("price" ->> newPrice) // Update an existing field
updated: ... complex type elided ... =
Benjamin Pierce :: Types and Programming Languages :: 262162091 :: 46.11 :: HNil
scala> updated("price")
res6: Double = 46.11
scala> val extended = updated + ("inPrint" ->> true) // Add a new field
extended: ... complex type elided ... =
Benjamin Pierce :: Types and Programming Languages :: 262162091 :: 46.11 :: true :: HNil
scala> val noId = extended - "id" // Removed a field
noId: ... complex type elided ... =
Benjamin Pierce :: Types and Programming Languages :: 46.11 :: true :: HNil
scala> noId("id") // Attempting to access a missing field is a compile time error
<console>:25: error: could not find implicit value for parameter selector ...
noId("id")
^
Joni Freeman's (@jonifreeman) sqltyped library makes extensive use of shapeless records.
shapeless has a Coproduct type, a generalization of Scala's Either
to an arbitrary number of choices. Currently it
exists primarily to support Generic
(see the next section), but will be expanded analogously to HList
in later
releases. Currently Coproduct
supports mapping, selection and unification,
scala> type ISB = Int :+: String :+: Boolean :+: CNil
defined type alias ISB
scala> val isb = Coproduct[ISB]("foo")
isb: ISB = foo
scala> isb.select[Int]
res0: Option[Int] = None
scala> isb.select[String]
res1: Option[String] = Some(foo)
object size extends Poly1 {
implicit def caseInt = at[Int](i => (i, i))
implicit def caseString = at[String](s => (s, s.length))
implicit def caseBoolean = at[Boolean](b => (b, 1))
}
scala> isb map size
res2: (Int, Int) :+: (String, Int) :+: (Boolean, Int) :+: CNil = (foo,3)
scala> res2.select[(String, Int)]
res3: Option[(String, Int)] = Some((foo,3))
In the same way that adding labels to the elements of an HList
gives us a record, adding labels to the elements of a
Coproduct
gives us a discriminated union,
scala> import record.RecordType, syntax.singleton._, union._
import record.RecordType
import syntax.singleton._
import union._
scala> val uSchema = RecordType.like('i ->> 23 :: 's ->> "foo" :: 'b ->> true :: HNil)
scala> type U = uSchema.Union
defined type alias U
scala> val u = Coproduct[U]('s ->> "foo") // Inject a String into the union at label 's
u: U = foo
scala> u.get('i) // Nothing at 'i
res0: Option[Int] = None
scala> u.get('s) // Something at 's
res1: Option[String] = Some(foo)
scala> u.get('b) // Nothing at 'b
res2: Option[Boolean] = None
Currently these exist primarily to support LabelledGeneric but, like Coproduct
s and records, will be further
developed in future releases.
The Iso
s of earlier shapeless releases have been completely reworked as the new Generic
type, which closely
resembles the generic programming capabilities introduced to GHC 7.2.
Generic[T]
, where T
is a case class or an abstract type at the root of a case class hierarchy, maps between values
of T
and a generic sum of products representation (HList
s and Coproduct
s),
scala> case class Foo(i: Int, s: String, b: Boolean)
defined class Foo
scala> val fooGen = Generic[Foo]
fooGen: shapeless.Generic[Foo]{ type Repr = Int :: String :: Boolean :: HNil } = $1$$1@724d2dfe
scala> val foo = Foo(23, "foo", true)
foo: Foo = Foo(23,foo,true)
scala> fooGen.to(foo)
res0: fooGen.Repr = 23 :: foo :: true :: HNil
scala> 13 :: res0.tail
res1: Int :: String :: Boolean :: HNil = 13 :: foo :: true :: HNil
scala> fooGen.from(res1)
res2: Foo = Foo(13,foo,true)
Typically values of Generic
for a given case class are materialized using an implicit macro, allowing a wide variety
of structural programming problems to be solved with no or minimal boilerplate. In particular the
existing lens, Scrap Your Boilerplate and generic zipper implementations are now
available for any case class family (recursive families included, as illustrated below) without any additional
boilerplate being required,
// Simple recursive case class family
sealed trait Tree[T]
case class Leaf[T](t: T) extends Tree[T]
case class Node[T](left: Tree[T], right: Tree[T]) extends Tree[T]
// Polymorphic function which adds 1 to any Int and is the identity
// on all other values
object inc extends ->((i: Int) => i+1)
val tree: Tree[Int] =
Node(
Node(
Node(
Leaf(1),
Node(
Leaf(2),
Leaf(3)
)
),
Leaf(4)
),
Node(
Leaf(5),
Leaf(6)
)
)
// Transform tree by applying inc everywhere
everywhere(inc)(tree)
// result:
// Node(
// Node(
// Node(
// Leaf(2),
// Node(
// Leaf(3),
// Leaf(4)
// )
// ),
// Leaf(5)
// ),
// Node(
// Leaf(6),
// Leaf(7)
// )
// )
A natural extension of Generic
's mapping of the content of data types onto a sum of products representation is to
a mapping of the data type including its constructor and field names onto a labelled sum of products representation,
ie. a representation in terms of the discriminated unions and records that we saw above. This is provided by
LabelledGeneric
. Currently it provides the underpinnings for the use of shapeless lenses with symbolic path
selectors (see next section) and it is expected that it will support many scenarios which would otherwise require the
support of hard to maintain special case macros.
scala> import record._, syntax.singleton._
import record._
import syntax.singleton._
scala> case class Book(author: String, title: String, id: Int, price: Double)
defined class Book
scala> val bookGen = LabelledGeneric[Book]
scala> val tapl = Book("Benjamin Pierce", "Types and Programming Languages", 262162091, 44.11)
tapl: Book = Book(Benjamin Pierce,Types and Programming Languages,262162091,44.11)
scala> val rec = bookGen.to(tapl) // Convert case class value to generic representation
rec: bookGen.Repr = Benjamin Pierce :: Types and Programming Languages :: 262162091 :: 44.11 :: HNil
scala> rec('price) // Access the price field symbolically, maintaining type information
res0: Double = 44.11
scala> bookGen.from(rec.updateWith('price)(_+2.0)) // type safe operations on fields
res1: Book = Book(Benjamin Pierce,Types and Programming Languages,262162091,46.11)
scala> case class ExtendedBook(author: String, title: String, id: Int, price: Double, inPrint: Boolean)
defined class ExtendedBook
scala> val bookExtGen = LabelledGeneric[ExtendedBook]
scala> bookExtGen.from(rec + ('inPrint ->> true)) // map values between case classes via generic representation
res2: ExtendedBook = ExtendedBook(Benjamin Pierce,Types and Programming Languages,262162091,44.11,true)
A combination of LabelledGeneric
and singleton-typed Symbol
literals supports boilerplate-free lens creation for
arbitrary case classes,
import shapeless._
// A pair of ordinary case classes ...
case class Address(street : String, city : String, postcode : String)
case class Person(name : String, age : Int, address : Address)
// Some lenses over Person/Address ...
val nameLens = lens[Person] >> 'name
val ageLens = lens[Person] >> 'age
val addressLens = lens[Person] >> 'address
val streetLens = lens[Person] >> 'address >> 'street
val cityLens = lens[Person] >> 'address >> 'city
val postcodeLens = lens[Person] >> 'address >> 'postcode
scala> val person = Person("Joe Grey", 37, Address("Southover Street", "Brighton", "BN2 9UA"))
person: Person = Person(Joe Grey,37,Address(Southover Street,Brighton,BN2 9UA))
scala> val age1 = ageLens.get(person) // Read field, note inferred type
age1: Int = 37
scala> val person2 = ageLens.set(person)(38) // Update field
person2: Person = Person(Joe Grey,38,Address(Southover Street,Brighton,BN2 9UA))
scala> val person3 = ageLens.modify(person2)(_ + 1) // Transform field
person3: Person = Person(Joe Grey,39,Address(Southover Street,Brighton,BN2 9UA))
scala> val street = streetLens.get(person3) // Read nested field
street: String = Southover Street
scala> val person4 = streetLens.set(person3)("Montpelier Road") // Update nested field
person4: Person = Person(Joe Grey,39,Address(Montpelier Road,Brighton,BN2 9UA))
Based on and extending Generic
and LabelledGeneric
, Lars Hupel (@larsr_h) has contributed the TypeClass
family of type classes, which provide automatic type class derivation facilities roughly equivalent to those available
with GHC as described in "A Generic Deriving Mechanism for Haskell". There is a description of an
earlier iteration of the Scala mechanism here, and examples of its use deriving Show
and Monoid
instances here and here for labelled coproducts and unlabelled products respectively.
For example, in the Monoid
case, once the general deriving infrastructure for monoids is in place, instances are
automatically available for arbitrary case classes without any additional boilerplate,
import MonoidSyntax._
import Monoid.auto._
// A pair of arbitrary case classes
case class Foo(i : Int, s : String)
case class Bar(b : Boolean, s : String, d : Double)
scala> Foo(13, "foo") |+| Foo(23, "bar")
res0: Foo = Foo(36,foobar)
scala> Bar(true, "foo", 1.0) |+| Bar(false, "bar", 3.0)
res1: Bar = Bar(true,foobar,4.0)
The shapeless-contrib project also contains automatically derived type class instances for Scalaz, Spire and Scalacheck.
Traversals and transformations of recursive types (eg. cons lists or trees) must themselves be recursive. Consequently type class instances which perform such operations must be recursive values in turn. This is problematic in Scala at both the value and the type levels: at the value level the issue is that recursive type class instances would have to be constructed lazily, whilst Scala doesn't natively support lazy implicit arguments; at the type level the issue is that during the type checking of expressions constructing recursive implicit values the implicit resolution mechanism would revisit types in a way that would trip the divergence checker.
The Lazy[T]
type constructor and associated macro in shapeless addresses both of these problems in many cases. It is
similar to Scalaz's Need[T]
and adds lazy implicit construction and suppression of divergence checking. This
supports constructions such as,
// Simple cons list
sealed trait List[+T]
case class Cons[T](hd: T, tl: List[T]) extends List[T]
sealed trait Nil extends List[Nothing]
case object Nil extends Nil
trait Show[T] {
def apply(t: T): String
}
object Show {
// Base case for Int
implicit def showInt: Show[Int] = new Show[Int] {
def apply(t: Int) = t.toString
}
// Base case for Nil
implicit def showNil: Show[Nil] = new Show[Nil] {
def apply(t: Nil) = "Nil"
}
// Case for Cons[T]: note (mutually) recursive implicit argument referencing Show[List[T]]
implicit def showCons[T](implicit st: Lazy[Show[T]], sl: Lazy[Show[List[T]]]): Show[Cons[T]] = new Show[Cons[T]] {
def apply(t: Cons[T]) = s"Cons(${show(t.hd)(st.value)}, ${show(t.tl)(sl.value)})"
}
// Case for List[T]: note (mutually) recursive implicit argument referencing Show[Cons[T]]
implicit def showList[T](implicit sc: Lazy[Show[Cons[T]]]): Show[List[T]] = new Show[List[T]] {
def apply(t: List[T]) = t match {
case n: Nil => show(n)
case c: Cons[T] => show(c)(sc.value)
}
}
}
def show[T](t: T)(implicit s: Show[T]) = s(t)
val l: List[Int] = Cons(1, Cons(2, Cons(3, Nil)))
// Without the Lazy wrappers above the following would diverge ...
show(l) // "Cons(1, Cons(2, Cons(3, Nil)))"
which would otherwise be impossible in Scala.
shapeless provides collection types with statically known sizes. These can prevent runtime errors such as those that would result from attempting to take the head of an empty list, and can also verify more complex relationships.
In the example below we define a method csv
whose signature guarantees at compile time that there are exactly as many
column headers provided as colums,
def row(cols : Seq[String]) =
cols.mkString("\\"", "\\", \\"", "\\"")
def csv[N <: Nat]
(hdrs : Sized[Seq[String], N],
rows : List[Sized[Seq[String], N]]) = row(hdrs) :: rows.map(row(_))
val hdrs = Sized("Title", "Author")
val rows = List(
Sized("Types and Programming Languages", "Benjamin Pierce"),
Sized("The Implementation of Functional Programming Languages", "Simon Peyton-Jones")
)
// hdrs and rows statically known to have the same number of columns
val formatted = csv(hdrs, rows) // Compiles
// extendedHdrs has the wrong number of columns for rows
val extendedHdrs = Sized("Title", "Author", "ISBN")
val badFormatted = csv(extendedHdrs, rows) // Does not compile
shapeless provides a Typeable
type class which provides a type safe cast operation. cast
returns an Option
of the
target type rather than throwing an exception if the value is of the incorrect type, as can happen with separate
isInstanceOf
and asInstanceOf
operations. Typeable
handles primitive values correctly and will recover erased
types in many circumstances,
import syntax.typeable._
val l: Any = List(Vector("foo", "bar", "baz"), Vector("wibble"))
l: Any = List(Vector(foo, bar, baz), Vector(wibble))
scala> l.cast[List[Vector[String]]]
res0: Option[List[Vector[String]]] = Some(List(Vector(foo, bar, baz), Vector(wibble)))
scala> l.cast[List[Vector[Int]]]
res1: Option[List[Vector[Int]]] = None
scala> l.cast[List[List[String]]]
res2: Option[List[List[String]]] = None
An extractor based on Typeable
is also available, allowing more precision in pattern matches,
scala> val `List[String]` = TypeCase[List[String]]
List[String]: shapeless.TypeCase[List[String]] = shapeless.TypeCase$$anon$16@14a9d20a
scala> val `List[Int]` = TypeCase[List[Int]]
List[Int]: shapeless.TypeCase[List[Int]] = shapeless.TypeCase$$anon$16@5810c269
scala> val l = List(1, 2, 3)
l: List[Int] = List(1, 2, 3)
scala> (l: Any) match {
| case `List[String]`(List(s, _*)) => s.length
| case `List[Int]`(List(i, _*)) => i+1
| }
res0: Int = 2
The equivalent pattern match without Typeable
/TypeCase
would result in a compile-time warning about the erasure
of the list's type parameter, then at runtime spuriously match the List[String]
case and fail with a
ClassCastException
while attempting to evaluate its right hand side.
Be aware that the increased precision and safety provided by Typeable
/TypeCase
don't alter the fact that type
caseing should be avoided in general other than at boundaries with external components which are intrinsically untyped
(eg. serialization points) or which otherwise have poor type discipline.
Libraries like shapeless which make extensive use of type-level computation and implicit resolution often need to
provide guarantees that certain expressions don't typecheck. Testing these guarantees is supported in shapeless via
the illTyped
macro,
import shapeless.test.illTyped
scala> illTyped { """1+1 : Boolean""" }
scala> illTyped { """1+1 : Int""" }
<console>:19: error: Type-checking succeeded unexpectedly.
Expected some error.
illTyped { """1+1 : Int""" }
^