├── .gitignore ├── project ├── build.properties ├── project │ └── Build.scala └── Build.scala ├── readme.txt └── src ├── main └── scala │ ├── TMap.scala │ ├── MacroHelpers.scala │ ├── select.scala │ ├── RecordType.scala │ └── Record.scala └── test └── scala ├── TMapTest.scala └── RecordTest.scala /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .flooignore 3 | .floo 4 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 0.13.8 2 | -------------------------------------------------------------------------------- /project/project/Build.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | object BuildBuild extends Build{ 4 | override lazy val settings = super.settings ++ Seq( 5 | scalacOptions ++= Seq( 6 | "-feature", "-deprecation", "-unchecked" 7 | ) 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | Compossible: Extensible records and type-indexed maps. 2 | 3 | Compossible is a new, easy to use library implementing extensible 4 | records and type-indexed maps (TMaps) based on intersection types. 5 | Extensible records are a more modular alternative to case classes. 6 | Type-indexed maps are type-safe maps without the need for key names. 7 | 8 | Example usage: 9 | src/test/scala/RecordTest.scala 10 | 11 | src/test/scala/TMapTest.scala 12 | 13 | Sbt setup: 14 | 15 | resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots" 16 | 17 | libraryDependencies += "org.cvogt" %% "compossible" % "0.2" 18 | -------------------------------------------------------------------------------- /src/main/scala/TMap.scala: -------------------------------------------------------------------------------- 1 | package org.cvogt.compossible 2 | import scala.reflect.runtime.universe._ 3 | 4 | /** 5 | A type-indexed map implementation with no tricks. 6 | You get what the type-system gives you, which already 7 | covers many use cases. 8 | */ 9 | class TMap[+T] private(private val values: List[(Type, Any)]) { 10 | def apply[E >: T](implicit tt: TypeTag[E]): E = { 11 | // runtime reflection doesn't allow constant time element lookup 12 | values.find(_._1 <:< tt.tpe).get._2.asInstanceOf[E] 13 | } 14 | 15 | /** 16 | Concatenate two TMaps. 17 | Elements of the left one override 18 | */ 19 | def ++[S](other: TMap[S]) 20 | = new TMap[T with S](other.values ++ values) 21 | 22 | override def toString = "TMap("+values.toString+")" 23 | } 24 | 25 | object TMap { 26 | def apply[T](values: T)(implicit tt: TypeTag[T]) = 27 | new TMap[T](List(tt.tpe -> values)) 28 | } 29 | -------------------------------------------------------------------------------- /src/main/scala/MacroHelpers.scala: -------------------------------------------------------------------------------- 1 | package org.cvogt.compossible 2 | import scala.reflect.macros.whitebox.Context 3 | import scala.language.experimental.macros 4 | 5 | trait MacroHelpers{ 6 | val c: Context 7 | import c.universe._ 8 | 9 | protected def error(msg: String) = c.error(c.enclosingPosition, msg) 10 | 11 | protected def firstTypeArg(tree: Tree) = { 12 | tree.tpe.widen.dealias.typeArgs.head 13 | } 14 | 15 | protected def prefixTypeArg 16 | = firstTypeArg(c.prefix.tree) 17 | 18 | protected def splitRefinedTypes(t: Type): Seq[Type] = t match { 19 | case RefinedType(types,scope) => types.map(splitRefinedTypes(_)).flatten 20 | case t => Seq(t) 21 | } 22 | 23 | protected def singletonConstant(tpe: Type): Constant 24 | = tpe match { 25 | case ConstantType(c:Constant) => c 26 | } 27 | 28 | protected def splitPair(t: Type) = { 29 | val args = t.typeArgs 30 | (args(0),args(1)) 31 | } 32 | 33 | protected def intersectTypes(types: Seq[Type]): Type 34 | = internal.refinedType( // TODO: can this be done with Quasi-Quotes? 35 | types.toList, internal.newScopeWith() 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/main/scala/select.scala: -------------------------------------------------------------------------------- 1 | package org.cvogt.compossible 2 | import scala.reflect.macros.whitebox.Context 3 | import scala.language.experimental.macros 4 | import scala.language.dynamics 5 | 6 | // structural by name selectors that allow 7 | // to describe sub-records for projections 8 | 9 | class select[T] extends Dynamic{ 10 | def &[Q](select: select[Q]): Any 11 | = macro selectMacros.unionMacro[Q] 12 | 13 | def applyDynamic(key: String) 14 | (value: org.cvogt.compossible.&.type) 15 | : Any 16 | = macro selectMacros.applyUnionMacro 17 | 18 | def selectDynamic(key: String): Any 19 | = macro selectMacros.selectUnionMacro 20 | } 21 | 22 | object & 23 | object select extends Dynamic{ 24 | def applyDynamic(key: String) 25 | (value: org.cvogt.compossible.&.type) 26 | : Any 27 | = macro selectMacros.applyCreateMacro 28 | 29 | def selectDynamic(key: String): Any 30 | = macro selectMacros.selectCreateMacro 31 | } 32 | 33 | class selectMacros(val c: Context) extends MacroHelpers{ 34 | import c.universe._ 35 | 36 | private def create(T: Type) = q"""new select[$T]""" 37 | 38 | private def union(Q: Type) 39 | = q"""new select[$prefixTypeArg with $Q]""" 40 | 41 | def unionMacro[Q:c.WeakTypeTag](select: Tree) 42 | = union(c.weakTypeTag[Q].tpe) 43 | 44 | def selectUnionMacro(key: Tree) 45 | = union(key.tpe) 46 | 47 | def applyUnionMacro(key: Tree)(value: Tree) 48 | = union(key.tpe) 49 | 50 | def selectCreateMacro(key: Tree) 51 | = create(key.tpe) 52 | 53 | def applyCreateMacro(key: Tree)(value: Tree) 54 | = create(key.tpe) 55 | } 56 | -------------------------------------------------------------------------------- /src/main/scala/RecordType.scala: -------------------------------------------------------------------------------- 1 | package org.cvogt.compossible 2 | 3 | import scala.reflect.macros.whitebox.Context 4 | import scala.language.experimental.macros 5 | import scala.language.dynamics 6 | 7 | // Workaround the current limitation of not being 8 | // able to express singleton types 9 | 10 | class RecordType[T <: (String, Any)] extends Dynamic{ 11 | type Type = Record[T] 12 | def &[Q <: (String, Any)](recordType: RecordType[Q]): Any 13 | = macro RecordTypeMacros.unionMacro[Q] 14 | 15 | def applyDynamic[V](key: String) 16 | (& : org.cvogt.compossible.&.type) 17 | : Any 18 | = macro RecordTypeMacros.applyUnionMacro[V] 19 | 20 | def selectDynamic[V](key: String): Any 21 | = macro RecordTypeMacros.selectUnionMacro[V] 22 | } 23 | 24 | object RecordType extends Dynamic{ 25 | def applyDynamic[V](key: String) 26 | (& : org.cvogt.compossible.&.type) 27 | : Any 28 | = macro RecordTypeMacros.applyCreateMacro[V] 29 | 30 | def selectDynamic[V](key: String): Any 31 | = macro RecordTypeMacros.selectCreateMacro[V] 32 | } 33 | 34 | class RecordTypeMacros(val c: Context) extends MacroHelpers{ 35 | import c.universe._ 36 | 37 | private def create(T: Type,V: Type) 38 | = q"""new RecordType[($T, $V)]""" 39 | 40 | private def union(Q: Type) 41 | = q"""new RecordType[$prefixTypeArg with $Q]""" 42 | 43 | private def append(K: Type, V: Type) 44 | = q"""new RecordType[$prefixTypeArg with ($K, $V)]""" 45 | 46 | def unionMacro[Q <: (String, Any):c.WeakTypeTag](recordType: Tree) 47 | = union(c.weakTypeTag[Q].tpe) 48 | 49 | def selectUnionMacro[V:c.WeakTypeTag](key: Tree) 50 | = append(key.tpe,c.weakTypeTag[V].tpe) 51 | 52 | def applyUnionMacro[V:c.WeakTypeTag](key: Tree)(& : Tree) 53 | = append(key.tpe,c.weakTypeTag[V].tpe) 54 | 55 | def selectCreateMacro[V:c.WeakTypeTag](key: Tree) 56 | = create(key.tpe,c.weakTypeTag[V].tpe) 57 | 58 | def applyCreateMacro[V:c.WeakTypeTag](key: Tree)(& : Tree) 59 | = create(key.tpe,c.weakTypeTag[V].tpe) 60 | } 61 | -------------------------------------------------------------------------------- /src/test/scala/TMapTest.scala: -------------------------------------------------------------------------------- 1 | package org.cvogt.test.compossible 2 | 3 | import org.scalautils.TypeCheckedTripleEquals._ 4 | import org.scalatest.FunSuite 5 | 6 | import org.cvogt.compossible.TMap 7 | import scala.reflect.runtime.universe._ 8 | 9 | class NativeTest extends FunSuite{ 10 | test("basic"){ 11 | assert(5 === TMap(5).apply[Int]) 12 | val t1 = TMap(5).apply[Int] 13 | assert(5 === t1) 14 | 15 | assert("test" === TMap("test").apply[String]) 16 | val t2 = TMap("test").apply[String] 17 | assert("test" === t2) 18 | } 19 | 20 | test("duplicate elements get last inserted"){ 21 | val t3 = TMap(5) ++ TMap(6) 22 | assert(6 == t3[Int]) 23 | assert(6 == t3[AnyVal]) 24 | assert(6 == t3[Any]) 25 | 26 | val t4 = TMap(6) ++ TMap(5) 27 | assert(5 == t4[Int]) 28 | assert(5 == t4[AnyVal]) 29 | assert(5 == t4[Any]) 30 | } 31 | 32 | case class Name(value: String) 33 | case class Age(value: Int) 34 | 35 | case class Model(value: String) 36 | test("test struct"){ 37 | type Person = TMap[Name with Age] 38 | val p: Person = TMap(Name("Chris")) ++ TMap(Age(99)) 39 | assert("Chris" === p[Name].value) 40 | assert(99 === p[Age].value) 41 | 42 | // subtyping property TMaps rely on for reader 43 | implicitly[TMap[Model with Age with Name] <:< TMap[Model] with TMap[Name with Age]] 44 | } 45 | 46 | case class _1(value: String) 47 | case class _2(value: Int) 48 | test("test tuple"){ 49 | type Tuple = TMap[_1 with _2] 50 | val p: Tuple = TMap(_1("Chris")) ++ TMap(_2(99)) 51 | assert("Chris" === p[_1].value) 52 | assert(99 === p[_2].value) 53 | } 54 | 55 | case class left(value: Int) 56 | case class right(value: Int) 57 | test("test named arguments"){ 58 | def plus(args: TMap[left with right]) 59 | = args[left].value + args[right].value 60 | 61 | assert{ 62 | val args = TMap(left(4)) ++ TMap(right(5)) 63 | 9 === plus(args) 64 | } 65 | } 66 | 67 | trait Database{ def query: Any } 68 | trait Logger { def log: Any } 69 | test("dependency injection test"){ 70 | implicit class FunctionReaderMonad[-T,+R](f: TMap[T] => R){ 71 | def map[Q](g: R => Q) = (t: TMap[T]) => g(f(t)) 72 | def flatMap[S,Q](g: R => (TMap[S] => Q)) = (ts: TMap[T with S]) => g(f(ts))(ts) 73 | } 74 | def use[T:TypeTag,R](f: T => R): TMap[T] => R = (c: TMap[T]) => f(c[T]) 75 | 76 | val f: TMap[Database with Logger] => Unit 77 | = for{ 78 | _ <- use{db: Database => db.query} 79 | _ <- use{logger: Logger => logger.log} 80 | } yield () 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /project/Build.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | 4 | object MyBuild extends Build{ 5 | val repoKind = SettingKey[String]("repo-kind", "Maven repository kind (\"snapshots\" or \"releases\")") 6 | 7 | lazy val aRootProject = Project(id = "compossible", base = file("."), 8 | settings = Seq( 9 | name := "compossible", 10 | scalaVersion := "2.11.7", 11 | description := "Composable Records and type-indexed Maps for Scala", 12 | libraryDependencies ++= Seq( 13 | "org.scalatest" %% "scalatest" % "2.2.5" % "test", 14 | "org.scalautils" %% "scalautils" % "2.1.7" % "test" 15 | ), 16 | libraryDependencies <+= scalaVersion( 17 | "org.scala-lang" % "scala-reflect" % _ //% "optional" 18 | ), 19 | scalacOptions ++= Seq("-feature", "-deprecation", "-unchecked"), 20 | //scalacOptions ++= Seq("-Xprint:patmat", "-Xshow-phases"), 21 | testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-oFD"), 22 | parallelExecution := false, // <- until TMap thread-safety issues are resolved 23 | version := "0.2-SNAPSHOT", 24 | organizationName := "Jan Christopher Vogt", 25 | organization := "org.cvogt", 26 | scalacOptions in (Compile, doc) <++= (version,sourceDirectory in Compile,name).map((v,src,n) => Seq( 27 | "-doc-title", n, 28 | "-doc-version", v, 29 | "-doc-footer", "Compossible is developed by Jan Christopher Vogt.", 30 | "-sourcepath", src.getPath, // needed for scaladoc to strip the location of the linked source path 31 | "-doc-source-url", "https://github.com/cvogt/compossible/blob/"+v+"/src/main€{FILE_PATH}.scala", 32 | "-implicits", 33 | "-diagrams", // requires graphviz 34 | "-groups" 35 | )), 36 | repoKind <<= (version)(v => if(v.trim.endsWith("SNAPSHOT")) "snapshots" else "releases"), 37 | //publishTo <<= (repoKind)(r => Some(Resolver.file("test", file("c:/temp/repo/"+r)))), 38 | publishTo <<= (repoKind){ 39 | case "snapshots" => Some("snapshots" at "https://oss.sonatype.org/content/repositories/snapshots") 40 | case "releases" => Some("releases" at "https://oss.sonatype.org/service/local/staging/deploy/maven2") 41 | }, 42 | publishMavenStyle := true, 43 | publishArtifact in Test := false, 44 | pomIncludeRepository := { _ => false }, 45 | makePomConfiguration ~= { _.copy(configurations = Some(Seq(Compile, Runtime, Optional))) }, 46 | licenses += ("Creative Commons Attribution-ShareAlike 4.0 International", url("https://creativecommons.org/licenses/by-sa/4.0/")), 47 | homepage := Some(url("http://github.com/cvogt/compossible")), 48 | startYear := Some(2015), 49 | pomExtra := 50 | 51 | 52 | cvogt 53 | Jan Christopher Vogt 54 | -5 55 | https://github.com/cvogt/ 56 | 57 | 58 | 59 | git@github.com:cvogt/compossible.git 60 | scm:git:git@github.com:cvogt/compossible.git 61 | 62 | ) 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /src/test/scala/RecordTest.scala: -------------------------------------------------------------------------------- 1 | package org.cvogt.test.records 2 | 3 | import org.scalautils.TypeCheckedTripleEquals._ 4 | import org.scalatest.FunSuite 5 | import org.cvogt.compossible._ 6 | import org.cvogt.compossible.{Record => R} 7 | import scala.language.postfixOps 8 | 9 | class RecordTest extends FunSuite { 10 | test("basic") { 11 | val r /*: Record[("name".type -> String)]*/ 12 | = Record name "Chris" 13 | 14 | assert("Chris" === r.name) 15 | 16 | val r2 = r.name = "Miguel" 17 | assert("Miguel" === r2.name) 18 | 19 | val r3 = r2 & (Record age 99) 20 | 21 | assert("Miguel" === r3.name) 22 | assert(99 === r3.age) 23 | 24 | 25 | val person = (Record name "Chris" 26 | age 99 27 | dob new java.util.Date()) 28 | 29 | val name = person.name 30 | val age = person.age 31 | val dob = person.dob 32 | 33 | (name: String, age: Int, dob: java.util.Date) 34 | 35 | 36 | val car = (Record owner "Chris" 37 | model "Mercedes") 38 | 39 | { 40 | val merged = for{ 41 | p <- List(person) 42 | c <- List(car) if c.owner == p.name 43 | } yield p & c 44 | 45 | merged.map{ 46 | r => 47 | assert(r.name === "Chris") 48 | assert(r.owner === "Chris") 49 | assert(r.model === "Mercedes") 50 | assert(r.age === 99) 51 | (r.name: String, r.age: Int, r.dob: java.util.Date, r.owner: String, r.model: String) 52 | } 53 | }; 54 | 55 | { 56 | val merged = for{ 57 | p <- List(person) 58 | c <- List(car) if c.owner == p.name 59 | } yield p(select name & age) & 60 | c(select.owner) 61 | 62 | merged.map{ 63 | r => 64 | assert(r.name === "Chris") 65 | assert(r.owner === "Chris") 66 | assertTypeError("""assert(r.model === "Mercedes")""") 67 | //assertTypeError("""r.dob: java.util.Date""") 68 | //assertTypeError("""r.model: String""") 69 | assert(r.age === 99) 70 | (r.name: String, r.age: Int, r.owner: String) 71 | } 72 | }; 73 | 74 | { 75 | val personWithCar = 76 | (Record name "Chris" 77 | age 99 78 | dob new java.util.Date() 79 | car (Record owner "Chris" 80 | model "Mercedes")) 81 | 82 | assert("Chris" === personWithCar.car.owner) 83 | 84 | //new Bar.Foo[String]().name[Int](5) 85 | 86 | val recordType = (RecordType age [Int] & 87 | name[String] & 88 | dob [java.util.Date] &) 89 | 90 | def foo(record: recordType.Type) = record.name 91 | 92 | assert("Chris" === foo(person)) 93 | assert("Chris" === foo(personWithCar)) 94 | 95 | //personWithCar.§.car(owner = "Miguel") 96 | }; 97 | 98 | { 99 | case class Person(name: String, age: Int, dob: java.util.Date) 100 | val t = Record.tuple(person) 101 | (t: (String, Int, java.util.Date),()) 102 | val p = Person.tupled(Record.tuple(person)) 103 | val r = Record.fromCaseClass(p) 104 | assert(r.name === "Chris") 105 | assert(r.age === 99) 106 | (r.dob,()) 107 | }; 108 | 109 | { 110 | // Good Example Use Case 111 | case class Person(name: String, age: Int) 112 | case class PersonWithDob(name: String, age: Int, dob: java.util.Date) 113 | val p1 = Person("Chris",99) 114 | val r = Record.fromCaseClass(p1) & 115 | (Record dob new java.util.Date) 116 | val p2 = PersonWithDob.tupled(Record.tuple(r)) 117 | }; 118 | 119 | { 120 | val r = 121 | (Record name "Chris") & 122 | (Record age 99) & 123 | (Record dob new java.util.Date()) 124 | 125 | val name = r.name 126 | val age = r.age 127 | val dob = r.dob 128 | 129 | r.age - 1 130 | 131 | assert("Chris" == r.name) 132 | assert(99 == r.age) 133 | 134 | assert("Chris" === r.name) 135 | assert(99 === r.age) 136 | 137 | val r2 = (r.name = "Miguel").age = 98 138 | assert("Miguel" === r2.name) 139 | assert(98 === r2.age) 140 | 141 | // val r3 = r(age = 98, name = Miguel) 142 | 143 | // val r3 = name = "Miguel").age = 98 144 | assert("Miguel" === r2.name) 145 | assert(98 === r2.age) 146 | 147 | //val r2 = r(update name & age) = ("") 148 | 149 | (name: String, age: Int, dob: java.util.Date) 150 | }; 151 | 152 | { // contrast what you can and can't do with a map 153 | val m = Map[String, Any]( 154 | "name" -> "Chris", "age" -> 99) 155 | 156 | assert("Chris" === m("name")) 157 | assert(99 === m("age")) 158 | 159 | // does not compile with a Map 160 | assertTypeError{ 161 | """ 162 | m("age") - 1 163 | """ 164 | } 165 | } 166 | /* 167 | { 168 | val name = person.extract("name").name 169 | val age = person.extract("age").age 170 | val dob = person.extract("dob").dob 171 | 172 | (name: String, age: Int, dob: java.util.Date) 173 | }; 174 | */ 175 | /* 176 | { 177 | val r = 178 | (((Record.name = "Chris") 179 | .age = 99) 180 | .dob = new java.util.Date()) 181 | 182 | val name = r.name 183 | val age = r.age 184 | val dob = r.dob 185 | 186 | (name: String, age: Int, dob: java.util.Date) 187 | }; 188 | */ 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/main/scala/Record.scala: -------------------------------------------------------------------------------- 1 | package org.cvogt.compossible 2 | 3 | import scala.reflect.macros.whitebox.Context 4 | import scala.language.experimental.macros 5 | import scala.language.dynamics 6 | 7 | // Extensible records for Scala based on intersection types 8 | 9 | object Record extends Dynamic{ 10 | def applyDynamic[K <: String](key: K)(value:Any): Record[(String, Any)] 11 | = macro RecordMacros.createMacro[K] 12 | 13 | def tuple(record: Record[_]): Product 14 | = macro RecordMacros.tupleMacro 15 | 16 | def fromCaseClass(obj: Product): Record[(String, Any)] 17 | = macro RecordMacros.fromCaseClassMacro 18 | //def updateDynamic[K <: String](key: K)(value:Any): Any = macro createMacro2[K] 19 | } 20 | 21 | // TODO make invariant and use implicit conversion macro freeing items from internal map 22 | class Record[+T <: (String,Any)]( 23 | val values: Map[String, Any] 24 | ) extends Dynamic{ 25 | override def toString = "Record("+values.toString+")" 26 | 27 | /** Combine two records into one. 28 | The combined one will have the keys of both. */ 29 | def &[O <: (String,Any)](other: Record[O]) 30 | = new Record[T with O](values ++ other.values) 31 | 32 | def selectDynamic[K <: String](key: K): Any 33 | = macro RecordMacros.lookupMacro[K] 34 | 35 | def apply[K <: String](select: select[K]): Record[(String, Any)] 36 | = macro RecordMacros.selectMacro[K] 37 | 38 | def updateDynamic[K <: String](key: K)(value:Any): Record[T] 39 | = new Record[T](values ++ Map(key -> value)) 40 | 41 | def applyDynamic[K <: String](key: K)(value:Any): Record[(String, Any)] 42 | = macro RecordMacros.appendFieldMacro[K] 43 | 44 | //def applyDynamicNamed[K <: String](key: K)(value:Any): Record[_] = macro RecordMacros. 45 | /* 46 | def selectDynamic[K <: String](k: K): Any 47 | = macro Record.extractMacro[K] 48 | 49 | def value: Any 50 | = macro Record.valueMacro 51 | */ 52 | //def updateDynamic[K <: String](key: K)(value:Any): Any = macro Record.createMacro3[K] 53 | } 54 | 55 | class RecordMacros(val c: Context) extends MacroHelpers{ 56 | import c.universe._ 57 | 58 | private def lookup(record: Tree, key: Tree, valueType: Type) 59 | = q"$record.values($key).asInstanceOf[$valueType]" 60 | 61 | private def byKey(typePairs: Seq[Type]) 62 | = typePairs.map(p => splitPair(p)._1 -> p).toMap 63 | 64 | private def keyValues(record: Tree) 65 | = splitRefinedTypes(firstTypeArg(record)) 66 | 67 | private def newRecord(tpe: Tree, keyValues: Seq[Tree]) 68 | = q"""new Record[$tpe](Map(..$keyValues))""" 69 | 70 | private def keyString(key: Tree) = 71 | key match{ 72 | case Literal(Constant("apply")) => 73 | error("Error: .apply is prohibited for Records.") 74 | ??? 75 | //q"""${c.prefix}""" 76 | case Literal(Constant(const)) => const 77 | case _ => 78 | error("Only string literals are allows as keys, not: "+key) 79 | ??? 80 | } 81 | 82 | def createRecord(key: Tree, value: Tree) 83 | = newRecord( 84 | tq"(${key.tpe}, ${value.tpe.widen})", 85 | Seq(q"${key} -> ${value}") 86 | ) 87 | 88 | def selectMacro[K <: String:c.WeakTypeTag] 89 | (select: Tree) 90 | = { 91 | val selectedTypes = splitRefinedTypes(c.weakTypeTag[K].tpe) 92 | 93 | newRecord( 94 | TypeTree(intersectTypes( 95 | selectedTypes.map(byKey(keyValues(c.prefix.tree))) 96 | )), 97 | 98 | selectedTypes.map{ 99 | case ConstantType(key:Constant) => 100 | q"$key -> ${c.prefix}.values($key)" 101 | } 102 | ) 103 | } 104 | 105 | 106 | def appendFieldMacro[K <: String:c.WeakTypeTag](key: Tree)(value: Tree) 107 | = { 108 | keyString(key) 109 | q"""${c.prefix.tree} & ${createRecord(key, value)}""" 110 | } 111 | 112 | def createMacro[K <: String:c.WeakTypeTag](key: Tree)(value: Tree) 113 | = createRecord(key, value) 114 | 115 | def lookupMacro[K <: String:c.WeakTypeTag](key: Tree) 116 | = { 117 | val valueType = 118 | keyValues(c.prefix.tree) 119 | .map(splitPair) 120 | .toMap 121 | .get(key.tpe) 122 | .getOrElse{ 123 | error(s"""Record has no key .${keyString(key)}""") 124 | ??? 125 | } 126 | lookup(c.prefix.tree, key, valueType) 127 | } 128 | /* 129 | def valueMacro: c.Expr[Any] 130 | = { 131 | import c.universe._ 132 | val recordTypeArg = c.prefix.actualType.widen.typeArgs.head 133 | 134 | def splitTypes(t: Type): Seq[Type] = t match { 135 | case RefinedType(types,scope) => types.map(splitTypes(_)).flatten 136 | case t => Seq(t) 137 | } 138 | val keyValuePairTypes: Seq[Type] = splitTypes(recordTypeArg) 139 | if(keyValuePairTypes.size >1){ 140 | c.error( 141 | c.enclosingPosition, 142 | ".value can only be called on single-element Records" 143 | ) 144 | } 145 | 146 | val Seq(ConstantType(key:Constant),v) = keyValuePairTypes.head.typeArgs 147 | c.Expr[Any](q"""${c.prefix}.values($key).asInstanceOf[$v]""") 148 | } 149 | 150 | def extractMacro[K <: String:c.WeakTypeTag] 151 | (k: c.Expr[K]): c.Expr[Any] 152 | = { 153 | val recordTypeArg = c.prefix.actualType.widen.typeArgs.head 154 | 155 | def splitTypes(t: Type): Seq[Type] = t match { 156 | case RefinedType(types,scope) => types.map(splitTypes(_)).flatten 157 | case t => Seq(t) 158 | } 159 | val keyValuePairTypes: Seq[Type] = splitTypes(recordTypeArg) 160 | 161 | val keyValueMap = keyValuePairTypes.map{ 162 | t => 163 | val args = t.typeArgs 164 | (args(0),args(1)) 165 | }.toMap 166 | 167 | val keyString = k.tree match{ 168 | case Literal(Constant("apply")) => c.error( 169 | c.enclosingPosition, 170 | "Error: You are trying to use \"apply\" as Record key or call a Record's apply method. Both are prohibited." 171 | ) 172 | case Literal(Constant(s)) => s 173 | case _ => c.error(c.enclosingPosition, "Only string literals are allows as keys, not: "+k.tree) 174 | } 175 | val kt = c.weakTypeTag[K] 176 | val v = keyValueMap.get(k.tree.tpe).getOrElse{ 177 | c.error( 178 | c.enclosingPosition, 179 | s"""Record has no key .$keyString""" 180 | ) 181 | ??? 182 | } 183 | c.Expr[Any](q"""new Record[(${k.tree.tpe},$v)](Map(${k.tree} -> ${c.prefix}.values(${k.tree}).asInstanceOf[$v]))""") 184 | } 185 | */ 186 | def tupleMacro(record: Tree) 187 | = { 188 | val accessors = 189 | keyValues(record) 190 | .map(splitPair) 191 | .map{ 192 | case (ConstantType(key:Constant),valueType) => 193 | lookup(record,Literal(key),valueType) 194 | } 195 | q"""(..$accessors)""" 196 | } 197 | 198 | def fromCaseClassMacro(obj: Tree) 199 | = { 200 | val tpe = obj.tpe.widen.dealias 201 | assert(tpe.typeSymbol.asInstanceOf[ClassSymbol].isCaseClass) 202 | 203 | val params = 204 | tpe.decls.collectFirst { 205 | case m: MethodSymbol if m.isPrimaryConstructor => m 206 | }.get.paramLists.head 207 | 208 | val names = params.map{ field => 209 | ( field.name.toTermName.decodedName.toString, 210 | field.typeSignature) 211 | } 212 | 213 | val keyValues = names.map{ 214 | case (name,tpe) => ( 215 | q"""${Constant(name)} -> ${obj}.${TermName(name)}""" 216 | ) 217 | } 218 | 219 | val types = names.map{ 220 | case (name,tpe) => ( 221 | tq"""(${internal.constantType(Constant(name))},${tpe})""" 222 | ) 223 | }.reduce((a,b) => tq"""$a with $b""") 224 | 225 | newRecord(types, keyValues) 226 | } 227 | } 228 | --------------------------------------------------------------------------------