Awesome
tagged-types
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" % "3.6.1"
Artifacts are published for Scala 2.12
/2.13
and Scala.js 1.5
.
API
Defining tagged types
import taggedtypes._
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 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.
Un-tagging
Immediate value:
val rawSheldon: String = sheldon.-@ // or sheldon.unTagged
Container value:
val rawUsers: List[String] = users.-@@ // or users.unTaggedF
Arbitrarily nested container value:
val rawArbitrarilyNested: Option[List[Try[String @@ UsernameTag]]] = taggedArbitrarilyNested.-@@@@ // or taggedArbitrarilyNested.unTaggedG
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.
Auto tagging
Sometimes it's convenient to automatically convert raw values into tagged ones, e.g. in REPL or when integrating with external APIs. To achieve this, an import taggedtypes.auto._
is required:
import taggedtypes.auto._
val sheldon: Username = "scooper"
Typeclass auto tagging
To automatically lift a raw value typeclass into a tagged one, an import taggedtypes.auto.typeclass._
import is required:
import taggedtypes.auto.typeclass._
implicitly[Typeclass[Username]]
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"
}
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 {
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"
}
Integrating with libraries
Circe
Helpers for manually lifting Circe encoders/decoders.
import io.circe._
import taggedtypes._
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 manually lifting 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]