treev-io / tagged-types

Zero-dependency boilerplate-free tagged types for Scala

Version Matrix

tagged-types

Build status Maven Central

Zero-dependency boilerplate-free tagged types for Scala.

Usage

sbt

Add the following to your build.sbt (replace %% with %%% for Scala.js):

libraryDependencies += "io.treev" %% "tagged-types" % "1.4"

Artifacts are published for Scala / Scala.js 2.11 and 2.12.

API

Defining tagged types

import io.treev.tag._

object username extends TaggedType[String]

It's helpful to define a type alias for convenience, e.g. in package object:

object username extends TaggedType[String]
type Username = username.Type

TaggedType provides the following members:

  • apply method to construct tagged type from raw values, e.g. username("scooper");
  • Tag trait to access the tag, e.g. List("scooper").@@@[username.Tag] (see below for container tagging);
  • Raw type member to access raw type, e.g. to help with type inference where needed:
object username extends TaggedType[String]
type Username = username.Type

case class User(name: Username)

val users = List(User(username("scooper")))
users.sortBy(_.name: username.Raw)
  • Type type member to access tagged type.

Tagging

Tagging values
sealed trait UsernameTag

val sheldon = "scooper".@@[UsernameTag]
sheldon: String @@ UsernameTag
// or "scooper".taggedWith[UsernameTag]

Or, if you have TaggedType instance:

object username extends TaggedType[String]

val sheldon = "scooper" @@ username
sheldon: String @@ username.Tag
sheldon: username.Type
// or "scooper" taggedWith username
// or username("scooper")
Tagging container values
val rawUsers = List("scooper", "lhofstadter", "rkoothrappali")
val users = rawUsers.@@@[UsernameTag]
users: List[String @@ UsernameTag]
// or rawUsers.taggedWithF[UsernameTag]

Can also tag using TaggedType instance as above.

Tagging arbitrarily nested container values
import scala.util.Try
val arbitrarilyNested = Some(List(Try("scooper"), Try("lhofstadter"), Try("rkoothrappali")))
val taggedArbitrarilyNested = arbitrarilyNested.@@@@[UsernameTag]
taggedArbitrarilyNested: Option[List[Try[String @@ UsernameTag]]]
// or arbitrarilyNested.taggedWithG[UsernameTag]

Can also tag using TaggedType instance as above.

Adding more tags

Immediate value:

sealed trait OwnerTag

val username = "scooper".@@[UsernameTag]
val owner = username.+@[OwnerTag]
owner: String @@ (UsernameTag with OwnerTag)
// or username.andTaggedWith[OwnerTag]

Container value:

val owners = users.+@@[OwnerTag]
owners: List[String @@ (UsernameTag with OwnerTag)]
// or users.andTaggedWithF[OwnerTag]

Arbitrarily nested container value:

val owners = taggedArbitrarilyNested.+@@@[OwnerTag]
owners: Option[List[Try[String @@ (UsernameTag with OwnerTag)]]]
// or taggedArbitrarilyNested.andTaggedWithG[OwnerTag]:

Can also tag using TaggedType instance as above.

Migrating from value classes

Suppose you have a value class:

case class Username(value: String) extends AnyVal {
  def isValid: Boolean = !value.isEmpty
}
object Username {
  val FieldName: String = "Username"
  
  implicit val ordering: Ordering[Username] = Ordering.by(_.value)
}

Then, it's a matter of changing it to:

object username extends TaggedType[String]

Any methods on original case class instance turn into implicit extensions:

object username extends TaggedType[String] {
  implicit class UsernameExtensions(val value: Type) 
    extends AnyVal { // still good application of value classes
  
    def isValid: Boolean = !value.isEmpty
  }
}

Any constants on original case class' companion object are merged into username object:

object username extends TaggedType[String] {
  val FieldName: String = "Username"
  
  implicit val ordering: Ordering[Type] = Ordering[String].@@@[Tag]
}

Note about implicit resolution

Implicit resolution won't work as it was before when using companion objects, so, to bring implicit Ordering instance or UsernameExtensions from above into scope, need to import it explicitly:

import username._
// or import username.ordering
// or import username.UsernameExtensions

Integrating with libraries

Circe

Helpers for defining Circe encoders/decoders.

import io.circe._
import io.treev.tag._

def taggedDecoder[T: Decoder, U]: Decoder[T @@ U] =
  Decoder.instance(_.as[T].@@@[U])

def taggedTypeDecoder[T: Decoder](taggedType: TaggedType[T]): Decoder[taggedType.Type] =
  taggedDecoder[T, taggedType.Tag]

def taggedEncoder[T: Encoder, U]: Encoder[T @@ U] =
  Encoder[T].@@@[U]

def taggedTypeEncoder[T: Encoder](taggedType: TaggedType[T]): Encoder[taggedType.Type] =
  taggedEncoder[T, taggedType.Tag]

Slick

Helpers for defining Slick column types.

import io.circe._
import scala.reflect.ClassTag
import slickProfile.api._

def taggedColumnType[T, U](implicit tColumnType: BaseColumnType[T],
                                    clsTag: ClassTag[T @@ U]): BaseColumnType[T @@ U] =
  MappedColumnType.base[T @@ U, T](identity, _.@@[U])

def taggedTypeColumnType[T](taggedType: TaggedType[T])
                           (implicit tColumnType: BaseColumnType[T],
                                     clsTag: ClassTag[taggedType.Type]): BaseColumnType[taggedType.Type] =
  taggedColumnType[T, taggedType.Tag]