All Downloads are FREE. Search and download functionalities are using the official Maven repository.

shapeless.records.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2011-16 Miles Sabin
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package shapeless

import scala.language.dynamics
import scala.language.experimental.macros

import scala.reflect.macros.whitebox

/**
 * Record operations on `HList`'s with field-like elements.
 *
 * @author Miles Sabin
 */
object record {
  import syntax.RecordOps

  implicit def recordOps[L <: HList](l : L) : RecordOps[L] = new RecordOps(l)

  /**
   * Records encoded as `HLists` of their value types intersected with the
   * singleton types of their keys.
   *
   * Record types may be written using a relatively concise syntax thanks to a trick
   * due to Denys Shabalin (@den_sh) and Eugene Burmako (@xeno_by). We use a
   * combination of `selectDynamic` and backticks to embed a type in a path which
   * appears to the compiler as stable,
   *
   * {{{
   * type Xyz = Record.`'x -> Int, 'y -> String, 'z -> Boolean`.T
   * }}}
   *
   * The use of singleton-typed `Symbols` as keys would make this type extremely
   * laborious to write out by hand.
   *
   * There is also a mechanism for creating values of record types using Scala's
   * named argument syntax. Values of the type just defined can be created as follows,
   *
   * {{{
   * val xyz = Record(x = 23, y = "foo", z = true)
   * xyz('y) // == "foo"
   * }}}
   */
  object Record extends Dynamic {
    def applyDynamic(method: String)(rec: Any*): HList = macro RecordMacros.mkRecordEmptyImpl
    def applyDynamicNamed(method: String)(rec: Any*): HList = macro RecordMacros.mkRecordNamedImpl

    def selectDynamic(tpeSelector: String): Any = macro LabelledMacros.recordTypeImpl
  }
}

/**
 * Trait supporting mapping named argument lists to record arguments.
 *
 * Mixing in this trait enables method applications of the form,
 *
 * {{{
 * lhs.method(x = 23, y = "foo", z = true)
 * }}}
 *
 * to be rewritten as,
 *
 * {{{
 * lhs.methodRecord('x ->> 23 :: 'y ->> "foo", 'z ->> true)
 * }}}
 *
 * ie. the named arguments are rewritten as record fields with the argument name
 * encoded as a singleton-typed `Symbol` and the application is rewritten to an
 * application of an implementing method (identified by the "Record" suffix) which
 * accepts a single record argument.
 */
trait RecordArgs extends Dynamic {
  def applyDynamic(method: String)(): Any = macro RecordMacros.forwardImpl
  def applyDynamicNamed(method: String)(rec: Any*): Any = macro RecordMacros.forwardNamedImpl
}

/**
 * Trait supporting mapping record arguments to named argument lists, inverse of RecordArgs.
 *
 * Mixing in this trait enables method applications of the form,
 *
 * {{{
 * lhs.methodRecord('x ->> 23 :: 'y ->> "foo" :: 'z ->> true :: HNil)
 * }}}
 *
 * to be rewritten as,
 *
 * {{{
 * lhs.method(x = 23, y = "foo", z = true)
 * }}}
 *
 * ie. the record argument is used to look up arguments for a target method
 * (the called method named minus the "Record" suffix) by name and type and the application
 * is rewritten to an application of the target method
 */
trait FromRecordArgs extends Dynamic {
  def applyDynamic[L <: HList](method: String)(rec: L): Any = macro RecordMacros.forwardFromRecordImpl[L]
}

@macrocompat.bundle
class RecordMacros(val c: whitebox.Context) {
  import c.universe._
  import internal.constantType
  import labelled.FieldType

  val hconsValueTree = reify {  ::  }.tree
  val hnilValueTree  = reify { HNil: HNil }.tree
  val fieldTypeTpe = typeOf[FieldType[_, _]].typeConstructor
  val SymTpe = typeOf[scala.Symbol]
  val atatTpe = typeOf[tag.@@[_,_]].typeConstructor

  def mkRecordEmptyImpl(method: Tree)(rec: Tree*): Tree = {
    if(rec.nonEmpty)
      c.abort(c.enclosingPosition, "this method must be called with named arguments")

    q"_root_.shapeless.HNil"
  }

  def mkRecordNamedImpl(method: Tree)(rec: Tree*): Tree = {
    val q"${methodString: String}" = method
    if(methodString != "apply")
      c.abort(c.enclosingPosition, s"this method must be called as 'apply' not '$methodString'")

    mkRecordImpl(rec: _*)
  }

  def forwardImpl(method: Tree)(): Tree = forwardNamedImpl(method)()

  def forwardNamedImpl(method: Tree)(rec: Tree*): Tree = {
    val lhs = c.prefix.tree 
    val lhsTpe = lhs.tpe

    val q"${methodString: String}" = method
    val methodName = TermName(methodString+"Record")

    if(lhsTpe.member(methodName) == NoSymbol)
      c.abort(c.enclosingPosition, s"missing method '$methodName'")

    val recTree = mkRecordImpl(rec: _*)

    q""" $lhs.$methodName($recTree) """
  }

  def forwardFromRecordImpl[L <: HList](method: Tree)(rec: Expr[L]): Tree = {
    val lhs = c.prefix.tree
    val lhsTpe = lhs.tpe

    val q"${methodString: String}" = method

    if (!methodString.matches(".*Record$"))
      c.abort(c.enclosingPosition, s"missing method '$methodString'")

    val methodName = TermName(methodString.replaceAll("Record$", ""))

    if(!lhsTpe.member(methodName).isMethod)
      c.abort(c.enclosingPosition, s"missing method '$methodName'")

    val params = mkParamsImpl(lhsTpe.member(methodName).asMethod, rec)
    q""" $lhs.$methodName(...$params) """
  }

  def mkSingletonSymbolType(c: Constant): Type =
    appliedType(atatTpe, List(SymTpe, constantType(c)))

  def mkFieldTpe(keyTpe: Type, valueTpe: Type): Type =
    appliedType(fieldTypeTpe, List(keyTpe, valueTpe.widen))

  def mkRecordImpl(rec: Tree*): Tree = {
    def mkElem(keyTpe: Type, value: Tree): Tree =
      q"$value.asInstanceOf[${mkFieldTpe(keyTpe, value.tpe)}]"

    def promoteElem(elem: Tree): Tree = elem match {
      case q""" $prefix(${Literal(k: Constant)}, $v) """ => mkElem(mkSingletonSymbolType(k), v)
      case _ =>
        c.abort(c.enclosingPosition, s"$elem has the wrong shape for a record field")
    }

    rec.foldRight(hnilValueTree) {
      case(elem, acc) => q""" $hconsValueTree(${promoteElem(elem)}, $acc) """
    }
  }

  def mkParamsImpl[L <: HList](method: MethodSymbol, rec: Expr[L]): List[List[Tree]] = {
    def mkElem(keyTpe: Type, value: Tree): Tree =
      q"_root_.scala.Predef.implicitly[_root_.shapeless.ops.hlist.Selector[${rec.actualType}, ${mkFieldTpe(keyTpe, value.tpe)}]].apply($rec)"

    method.paramLists.filterNot(_.forall(x => x.isImplicit)).map(_.map { x =>
      mkElem(mkSingletonSymbolType(Constant(x.name.decodedName.toString)), q"${x.typeSignature}")
    })

  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy