SkillAgentSearch skills...

Dilate

Nearly zero runtime object allocation powered by scalameta. Value class and Unboxed Tagged Type generation at compile-time.

Install / Use

/learn @vitorsvieira/Dilate

README

Dilate

Build Status Software License Maven Central

Overview

Dilate provides macro annotations that generates value classes and unboxed tagged types at compile-time for extra type-safety focusing on nearly zero runtime overhead. Value classes and tagged types have an important role when developing high performance Scala applications.

This project proudly uses scalameta.

Motivation

  • Type-safety and type checking
  • Avoid runtime object allocation
  • Zero syntactical and bytecode overhead

Getting Started

To get started with SBT, simply add the following to your build.sbt file:

libraryDependencies += "com.vitorsvieira" %% "dilate" % "0.1.2"

resolvers += Resolver.bintrayIvyRepo("scalameta", "maven")

// A dependency on macro paradise is required to both write and expand
// new-style macros.  This is similar to how it works for old-style macro
// annotations and a dependency on macro paradise 2.x.
addCompilerPlugin("org.scalameta" % "paradise" % "3.0.0-beta4" cross CrossVersion.full)

scalacOptions += "-Xplugin-require:macroparadise"

Examples

@valueclass

Applying @valueclass to the BankAccount class as below:

@valueclass case class BankAccount(
    number:      BigInt          = 10,
    funds:       BigDecimal,
    withdrawals: Seq[BigDecimal],
    token:       java.util.UUID) {

  def methodA = number * 1000
}

object BankAccount {
  def renew(account: BankAccount) = account.copy(token = java.util.UUID.randomUUID())
}

Allows BankAccount to be instantiated using the new types as:

val account = BankAccount(
  BankAccount.Number(1234),
  BankAccount.Funds(10),
  BankAccount.Withdrawals(Seq(1000)),
  BankAccount.Token(java.util.UUID.randomUUID())
)

The above construction is possible as the following types(value classes) and implicit conversions are generated at compile-time.

case class BankAccount(
  number:      BankAccount.Number = BankAccount.Number(10),
  funds:       BankAccount.Funds,
  withdrawals: BankAccount.Withdrawals,
  token:       BankAccount.Token) { 
  def methodA = number * 1000 
}
object BankAccount {
  final case class Number(self: BigInt) extends AnyVal
  final case class Funds(self: BigDecimal) extends AnyVal
  final case class Withdrawals(self: _root_.scala.collection.Seq[BigDecimal]) extends AnyVal
  final case class Token(self: java.util.UUID) extends AnyVal
  
  private[this] implicit def toNumber(number: BigInt): BankAccount.Number = BankAccount.Number(number)
  implicit def toBigIntfromNumber(number: Number): BigInt = number.self
  
  private[this] implicit def toFunds(funds: BigDecimal): BankAccount.Funds = BankAccount.Funds(funds)
  implicit def toBigDecimalfromFunds(funds: Funds): BigDecimal = funds.self
  
  private[this] implicit def toWithdrawals(withdrawals: Seq[BigDecimal]): BankAccount.Withdrawals = BankAccount.Withdrawals(withdrawals)
  implicit def toSeqBigDecimalfromWithdrawals(withdrawals: Withdrawals): Seq[BigDecimal] = withdrawals.self
  
  private[this] implicit def toToken(token: java.util.UUID): BankAccount.Token = BankAccount.Token(token)
  implicit def tojavautilUUIDfromToken(token: Token): java.util.UUID = token.self
  
  def renew(account: BankAccount) = account.copy(token = java.util.UUID.randomUUID())
}

@valueclass with @hold

@hold allows keeping the type without any modification. Applying @valueclass to the Person class as below:

case class Age(value: Int) extends AnyVal

@valueclass final class Person(
  v1:           Boolean,
  @hold v2:     Age                 = Age(1),//will hold Age as is
  v3:           Int                 = 1,
  v4:           Int,
  bankAccount: BankAccount.Number)

Generates the following types as value classes, and implicit conversions at compile-time.

sealed class Person(
  v1:          Person.V1,
  v2:          Age          = Age(1),        //held the type Age from the above value class 
  v3:          Person.V3    = Person.V3(1),
  v4:          Person.V4,
  bankAccount: BankAccount.Number)
  
object Person {
  final case class V1(self: Boolean) extends AnyVal
  final case class V3(self: Int) extends AnyVal
  final case class V4(self: Int) extends AnyVal
  
  private[this] implicit def toV1(v1: Boolean): Person.V1 = Person.V1(v1)
  implicit def toBooleanfromV1(v1: V1): Boolean = v1.self
  
  private[this] implicit def toV3(v3: Int): Person.V3 = Person.V3(v3)
  implicit def toIntfromV3(v3: V3): Int = v3.self
  
  private[this] implicit def toV4(v4: Int): Person.V4 = Person.V4(v4)
  implicit def toIntfromV4(v4: V4): Int = v4.self
}

@newtype

Applying @newtype to the BankAccount class as below:

import BankAccount._
@newtype case class BankAccount(
  activated:     Boolean           = true,
  number:        BigInt,
  funds:         BigDecimal,
  withdrawals:   Seq[BigDecimal],
  token:         java.util.UUID,
  @hold manager: String)

Allows BankAccount to be created like:

val bankAccount2 = OtherBankAccount(
  number      = BigInt(10).number,
  funds       = BigDecimal(10).funds,
  withdrawals = Seq(BigDecimal(10)).withdrawals,
  token       = java.util.UUID.randomUUID().token,
  manager     = "test"
)

Due to current limitations in the whiteboxing architecture, only when using @newtype macro, the construction must have named arguments.

The above construction is possible as the following Unboxed Tagged Types and implicit classes are generated at compile-time.

import BankAccount._
case class BankAccount(
  activated:   BankAccount.Activated   = true.activated,
  number:      BankAccount.Number, 
  funds:       BankAccount.Funds,
  withdrawals: BankAccount.Withdrawals,
  token:       BankAccount.Token,
  manager:     String)
  
object BankAccount {
  trait ActivatedTag
  trait NumberTag
  trait FundsTag
  trait WithdrawalsTag
  trait TokenTag
  
  type Activated   = Boolean         @@ ActivatedTag
  type Number      = BigInt          @@ NumberTag
  type Funds       = BigDecimal      @@ FundsTag
  type Withdrawals = Seq[BigDecimal] @@ WithdrawalsTag
  type Token       = java.util.UUID  @@ TokenTag
  
  implicit class TaggedBoolean(val value: Boolean) extends AnyVal { 
    def activated: Activated = value.asInstanceOf[Activated] 
  }
  implicit class TaggedBigInt(val value: BigInt) extends AnyVal { 
    def number: Number = value.asInstanceOf[Number] 
  }
  implicit class TaggedBigDecimal(val value: BigDecimal) extends AnyVal { 
    def funds: Funds = value.asInstanceOf[Funds] 
  }
  implicit class TaggedSeqBigDecimal(val value: Seq[BigDecimal]) extends AnyVal { 
    def withdrawals: Withdrawals = value.asInstanceOf[Withdrawals] 
  }
  implicit class TaggedjavautilUUID(val value: java.util.UUID) extends AnyVal { 
    def token: Token = value.asInstanceOf[Token] 
  }
}

Note that in the example above import BankAccount._ is presented. This is required only when using @newtype annotation and specially for classes with default values and/or companion objects with values looking for implicit conversion. This is required due to current limitations on macro whiteboxing.

All the examples above are available in the examples folder.

Bytecode Analysis

Using javap -p BankAccountWithoutMacro\$ BankAccountWithValueclass\$ BankAccountWithNewtype\$ to disassemble and see the bytecode generated with and without the macros, the following output shows the intended types being used!

Without any macro

Warning: Binary file BankAccountWithoutMacro$ contains com.vitorsvieira.dilate.BankAccountWithoutMacro$
Compiled from "Examples.scala"
public final class com.vitorsvieira.dilate.BankAccountWithoutMacro$ implements scala.Serializable {
  public static final com.vitorsvieira.dilate.BankAccountWithoutMacro$ MODULE$;

  private final java.lang.String field;

  private volatile boolean bitmap$init$0;

  public static {};
    Code:
       0: new           #2                  // class com/vitorsvieira/dilate/BankAccountWithoutMacro$
       3: invokespecial #14                 // Method "<init>":()V
       6: return

  public java.lang.String field();
    Code:
       0: aload_0
       1: getfield      #21                 // Field bitmap$init$0:Z
       4: ifeq          17
       7: aload_0
       8: getfield      #23                 // Field field:Ljava/lang/String;
      11: pop
      12: aload_0
      13: getfield      #23                 // Field field:Ljava/lang/String;
      16: areturn
      17: new           #25                 // class scala/UninitializedFieldError
      20: dup
      21: ldc           #27                 // String Uninitialized field: Examples.scala: 85
      23: invokespecial #30                 // Method scala/UninitializedFieldError."<init>":(Ljava/lang/String;)V
      26: athrow
    LineNumberTable:
      line 85: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      27     0  this   Lcom/vitorsvieira/dilate/BankAccountWithoutMacro$;

  public com.vitorsvieira.dilate.BankAccountWithoutMacro renew(com.vitorsvieira.dilate.BankAccountWithoutMacro);
    Code:
       0: invokestatic  #39                 // Method java/util/UUID.randomUUID:()Ljava/util/UUID;
       3: astore_2
       4: aload_1
       5: invokevirtual #45                 // Method com/vitorsvieira/dilate/BankAccountWithoutMacro.copy$default$1:()Z
       8: istore_3
       9: aload_1
      10: invokevirtual #49                 // Method com/vitorsv
View on GitHub
GitHub Stars79
CategoryDevelopment
Updated5mo ago
Forks4

Languages

Scala

Security Score

97/100

Audited on Oct 23, 2025

No findings