Skip to content

Instantly share code, notes, and snippets.

@IainHull
Created November 5, 2014 09:59
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save IainHull/16fafd2ed6dd19622493 to your computer and use it in GitHub Desktop.
Save IainHull/16fafd2ed6dd19622493 to your computer and use it in GitHub Desktop.
Scala Type-safe Wrapper

Something small I have been working on to enhance basic types with type-safe wrappers to enforce invariants at compile time. Reading valid values from configuration was the initial driver for me to experiment with this.

I have just seen that @bvenners is working on something similar in Scalactic

I really like his approach, here is a quick comparison with Scalactic's version

  • it includes versions for Long, Float and Double; I just include Int
  • it uses macros to ensure that basic construction only uses a valid Literal (this is an awesome idea!!!!)
  • it uses Options where I use Try
  • it provides sophisticated operator overloading for addition, I only provide basic operator support for percentage
  • I have abstracted the companion object for wrapper types
    • This is currently too verbose, I am still experimenting with different approaches here
    • The WrapperValue.Companion provides a single location for implementing type class instances like Ordering and ConfigReader
    • The WrapperValue.Companion provides an unapply extractor
package org.iainhull.wrapped
/**
* Value class to represent aimport com.workday.scala.util.WrappedValue
* positive int. Use this type to guarantee that and int value will always
* be positive.
*
* This is implemented with WrappedValue which is simplifies implementing classes
* that wrap a single value.
*
* {{{
* // Construction
* scala> PositiveInt(5)
* res2: org.iainhull.myconfig.PositiveInt.WrappedType = PositiveInt(5)
*
* // Validation
* scala> PositiveInt(-4)
* java.lang.IllegalArgumentException: requirement failed: -4 must be positive
*
* // Supports ordering based on Int
* scala> Seq(PositiveInt(3), PositiveInt(2), PositiveInt(1)).sorted
* res4: Seq[org.iainhull.myconfig.PositiveInt.WrappedType] = List(PositiveInt(1), PositiveInt(2), PositiveInt(3))
*
* // Supports pattern matching (extraction)
* scala> PositiveInt(42) match {
* | case PositiveInt(v) => println(v)
* | }
* 42
* }}}
*/
class PositiveInt private (val value: Int) extends AnyVal with WrappedValue[Int]
object PositiveInt extends WrappedValue.Companion {
type InnerType = Int
type WrappedType = PositiveInt
override protected def construct(value: Int): PositiveInt = new PositiveInt(value)
override protected def validate(value: Int): Option[String] = {
if (value < 0) Some(value + ": must be positive") else None
}
}
/**
* PercentageLike adds support for Percentage types. These are
* wrapped integers that include the `*` operator which scales
* other integers.
*/
trait PercentageLike extends Any with WrappedValue[Int] {
def value: Int
def * (other: Int): Int = value * other / 100
}
object PercentageLike {
implicit def toRichInt(value: Int): RichInt = new RichInt(value)
class RichInt(value: Int) {
def * (other: PercentageLike): Int = other * value
}
}
/**
* Basic Percentage type
*/
class Percentage private (val value: Int) extends AnyVal with PercentageLike
object Percentage extends WrappedValue.Companion {
type InnerType = Int
type WrappedType = Percentage
override protected def construct(value: Int) = new Percentage(value)
override protected def validate(value: Int) = None
}
/**
* Strict Percentage is a value between 0 and 100.
*/
class StrictPercentage private (val value: Int) extends AnyVal with PercentageLike
object StrictPercentage extends WrappedValue.Companion {
type InnerType = Int
type WrappedType = StrictPercentage
override protected def construct(value: Int) = new StrictPercentage(value)
override protected def validate(value: Int) = {
if (value < 0 || value > 100) Some(value + ": must be between 0 and 100 (inclusive)") else None
}
}
package org.iainhull.wrapped
class EmailAddress private (val value: String) extends AnyVal with WrappedValue[String]
object EmailAddress extends WrappedValue.Companion {
type InnerType = String
type WrappedType = EmailAddress
override protected def construct(value: String) = new EmailAddress(value)
val regex = "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}$".r
override protected def validate(value: String): Option[String] = {
if (regex.findAllMatchIn(value).isEmpty) Some(s"$value must be a valid email address") else None
}
}
package org.iainhull.wrapped
import scala.util.{ Try, Success, Failure }
import scala.util.control.NoStackTrace
/**
* Utility trait for value classes (see http://docs.scala-lang.org/overviews/core/value-classes.html)
*
* Classes that mix in this trait must implement the `value` method and then are provided
* with sensible versions of `toString`, `equals` and `hashCode` that delegate to the
* `value`s implementations.
*
* Classes that use this trait should use a private constructor and use WrappedValue.Companion to
* define their companion object. See PositiveInt for an example.
*/
trait WrappedValue[T] extends Any {
def value: T
override def toString = this.getClass.getSimpleName + "(" + value.toString + ")"
override def equals(other: Any): Boolean = {
if (this.getClass.isInstance(other)) {
value.equals(other.asInstanceOf[WrappedValue[T]].value)
} else {
false
}
}
override def hashCode: Int = value.hashCode
}
object WrappedValue {
abstract class Companion {
type InnerType
type WrappedType <: WrappedValue[InnerType]
protected def construct(value: InnerType): WrappedType
protected def validate(value: InnerType): Option[String]
/**
* Validate and construct the WrappedType
*
* @throws IllegalArgumentException is the value is not valid for WrappedType
*
* {{{
* val v = PositiveInt(42)
* }}}
*/
def apply(value: InnerType): WrappedType = {
validate(value) foreach { message => require(false, message) }
construct(value)
}
/**
* Validate and construct the WrappedType (without throwing exceptions)
*
* {{{
* val tryV: Try[PositiveInt] = PositiveInt.from(42)
* }}}
*/
def from(value: InnerType): Try[WrappedType] = {
validate(value) match {
case Some(message) => Failure(new IllegalArgumentException(message) with NoStackTrace)
case None => Success(construct(value))
}
}
/**
* Support extracting the `value` from a wrapped type.
*
* {{{
* PositiveInt(5) match {
* case PositiveInt(v) => println(v) // prints 5
* }
* }}}
*/
def unapply(wrapped: WrappedType): Option[InnerType] = Some(wrapped.value)
/**
* Provides an implicit ordering for the WrappedType iff InnerType supports an implicit ordering
*/
implicit def ordering(implicit ord: Ordering[InnerType]): Ordering[WrappedType] = new WrappedOrdering(ord)
class WrappedOrdering(ord: Ordering[InnerType]) extends Ordering[WrappedType] {
override def compare(x: WrappedType, y: WrappedType): Int = ord.compare(x.value, y.value)
}
/**
* Provide an implicit ConfigReader iff the InnerType supports an implicit ConfigReader
*/
implicit def configReader(implicit reader: ConfigReader[InnerType]): ConfigReader[WrappedType] = new WrappedConfigReader(reader)
class WrappedConfigReader(reader: ConfigReader[InnerType]) extends ConfigReader[WrappedType] {
def tryParse(value: String): Try[WrappedType] = {
reader.tryParse(value) flatMap (inner => Try(apply(inner)))
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment