Scala - Literal Types and Compile-Time Arithmetic



You can define types that correspond to specific values. These types are used for enforcing constraints. You can use certain values correctly throughout your code. You can write more precise code by combining literal types with arithmetic operations.

Literal Types

Literal types are types that have a single possible value. You can create for various basic types like integers, floats, strings, and booleans.You can create variables that are constrained to specific values using literal types.

Example

val one: 1 = 1
val trueLiteral: true = true
val hello: "hello" = "hello"

Benefits of Literal Types

  • Type Safety − You can use literal types for only specific values assigned to variables. You can prevent accidental errors.
  • Code Readability − Your Code becomes more expressive and easier to understand when literal types are used.
  • Compile-Time Error Checking − Compiler can catch more errors at compile-time to reduce runtime issues.

Using Literal Types in Arithmetic Operations

Literal type arithmetic to perform arithmetic operations on literal types. You can ensure type safety and consistency through compile-time checks. This can be used in generic programming where operations depend on specific values.

Defining Literal Types for Arithmetic

You use singleton types and appropriate type constraints to define literal types for arithmetic operations. For example -

val three: 3 = 3
val four: 4 = 4

Singleton Types

Variables can be typed with the exact value it holds. This is used for literal type arithmetic, where operations involve specific values.

Example

val x: 1 = 1
val y: x.type = x  // y has the type of the value x

Arithmetic Operations with Literal Types

When performing arithmetic operations using literal types, Scala ensures that these operations are type-safe and consistent.

Addition

You can define a method that takes two singleton types and returns their sum as a literal type. For example -

import singleton.ops._

def add[A <: Int with Singleton, B <: Int with Singleton](a: A, b: B): A + B = a + b

Subtraction

You can subtract two literal types, like this -

def subtract[A <: Int with Singleton, B <: Int with Singleton](a: A, b: B): A - B = a - b

Multiplication

You can multiply two literal types, like this -

def multiply[A <: Int with Singleton, B <: Int with Singleton](a: A, b: B): A * B = a * b

Division

You can divide two literal types. But you should avoid the divisor is not zero, like this -

def divide[A <: Int with Singleton, B <: Int with Singleton](a: A, b: B)
   (implicit ev: B =:!= 0): A / B = a / b

These are some advanced use cases of literal type arithmetic.

Dimensional Analysis

You can enforce constraints in dimensional analysis with type-safe operations involving units. For example -

import spire.implicits._
import libra._
import libra.si._

val distance: Quantity[Double, Meter] = 5.m
val time: Quantity[Double, Second] = 2.s
val speed: Quantity[Double, Meter / Second] = distance / time

Type-Safe Vector Arithmetic

You can create type-safe vector operations that only compile if dimensions match using the shapeless library. For example -

 import shapeless.Witness

trait Vec[N <: Int with Singleton]

object Vec {
  def apply[N <: Int with Singleton](implicit w: Witness.Aux[N]): Vec[N] = new Vec[N] {}
}

val vec3 = Vec[Witness.`3`.T]
val vec4 = Vec[Witness.`4`.T]

// This won't compile as dimensions don't match
// val vecSum = vec3 ++ vec4

Compile-Time Arithmetic with Singleton Types

You can perform compile-time arithmetic using literal. You can enforce constraints and ensure type safety using singleton types. For example -

import singleton.ops._

class Vec[L <: Int with Singleton] {
  def doubleSize: Vec[L * 2] = new Vec[L * 2]
  def nSize[N <: Int with Singleton]: Vec[N * L] = new Vec[N * L]
  def getLength(implicit length: SafeInt[L]): Int = length.value
}

object Vec {
  def apply[L <: Int with Singleton](implicit w: Witness.Aux[L]): Vec[L] = new Vec[L]
}

val vec10 = Vec[Witness.`10`.T]
val vec20 = vec10.doubleSize
val vec50 = vec10.nSize[Witness.`5`.T]

Literal Types with Macros

Macros are used to work with literal types, compile-time computation and validation. So, you can have advanced literal type arithmetic.

Example

Note that you cannot define and use macros within the same compilation unit. You need to separate the macro definition from its usage. You must split the code into two different files or even two different projects/modules.

For this example, we have created two files named Macros.scala and Main.scala in the Scala folder. This folder is look like this:

MyMacrosProject/
 build.sbt
 macros/
    build.sbt
    src/
        main/
            scala/
                Macros.scala
 main/
     build.sbt
     src/
         main/
             scala/
                 Main.scala

Also, your build.sbt file should be this before executing these macros codes -

import Dependencies._

ThisBuild / scalaVersion := "2.13.14"

lazy val macros = (project in file("macros"))
  .settings(
    name := "macros",
    libraryDependencies ++= Seq(
      "org.scala-lang" % "scala-reflect" % scalaVersion.value,
      "org.scala-lang" % "scala-compiler" % scalaVersion.value % Provided
    )
  )

lazy val main = (project in file("main"))
  .settings(
    name := "main",
    libraryDependencies ++= Seq(
      "org.scala-lang" % "scala-reflect" % scalaVersion.value
    )
  ).dependsOn(macros)

Now, your Macros.scala code should be this -

import scala.reflect.macros.blackbox.Context
import scala.language.experimental.macros

object LiteralMacros {
  def addLiterals(a: Int, b: Int): Int = macro addLiteralsImpl

  def addLiteralsImpl(c: Context)(a: c.Expr[Int], b: c.Expr[Int]): c.Expr[Int] = {
    import c.universe._
    (a.tree, b.tree) match {
      case (Literal(Constant(aVal: Int)), Literal(Constant(bVal: Int))) =>
        val result = aVal + bVal
        c.Expr[Int](Literal(Constant(result)))
      case _ =>
        c.abort(c.enclosingPosition, "Both arguments must be literal integers")
    }
  }
}

And, your Main.scala code should be this -

object Main extends App {
  val sum = LiteralMacros.addLiterals(3, 4) // Evaluates to 7 at compile time
  println(s"Sum: $sum")
}

Now, you can compile and execute using following commands -

Commands

\> sbt clean compile
\> sbt main/run

Output

Sum: 7

Notes

  • You can define types that correspond to specific values. These are used for enforcing constraints and ensuring correct values throughout your code.
  • It is possible to create variables with constrained values like val one: 1 = 1 and val hello: "hello" = "hello".
  • There are several benefits, including type safety, code readability, and compile-time error checking to reduce runtime issues.
  • You can perform type-safe arithmetic using methods like add, subtract, multiply, and divide for literal types.
  • It is possible to use singleton types to have variables with exact value types for precise operations, e.g., val x: 1 = 1.
  • There are advanced use cases, like enforcing constraints in dimensional analysis and creating type-safe vector operations using libraries like shapeless.
  • You can use macros for compile-time computation and validation with literal types for advanced arithmetic operations.
Advertisements