Home

Awesome

Build status Maven Central

Tagged & Newtypes - the better and much friendlier alternative to AnyVals.

supertagged for scala

SBT

Scala:

libraryDependencies += "org.rudogma" %% "supertagged" % "2.0-RC2"

ScalaJS

libraryDependencies += "org.rudogma" %%% "supertagged" % "2.0-RC2"

Contents

  1. Bytecode
  2. Tagged Types
    1. Basics
    2. Postfix syntax
    3. Overtagged
  3. Newtypes
    1. Basics
    2. Newtypes Custom
    3. More Examples
  4. Refined
  5. Lifting typeclasses for tagged & newtypes
  6. Unapply
  7. Migration from 1.4 to 2.x
  8. Classic way
  9. Alternatives
  10. Tests

Bytecode

For those who want to check bytecode, have a look at

Tagged Types

Basics

object Step extends TaggedType[Raw]{
    //...implicit scope for Step ...
    //...put here all implicits you need and they will be found here without additional imports...
    //...if you want to add more operations to Step, just define one more implicit class with ops...
    
    implicit final class Ops(private val value:Type) extends AnyVal {
        //... your methods here ...    
    }
}
type Step = Step.Type
object WidthT extends TaggedTypeT {
    type Raw[T] = T
}
type WidthT[T] = WidthT.Type[T]

val v1:WidthT[Int] = WidthT[Int](5) // WidthT.apply[Int](5)
val v2:WidthT[Long] = WidthT[Long](5L) 
object Counters extends TaggedType[T]{
  type Raw[T] = Array[T]
}
type Counters[T] = Counters.Type[T]

val v1:Counters[Long] = Counters[Long](Array(5L))
val v2:Counters[String] = Counters[String](Array("String"))

Nested tagging

val arr:Array[Array[Array[Array[Int]]]]
val v1 = Width(arr) // searches for first Int and replaces it -> Array[Array[Array[Array[Width]]]]
val v2 = WidthT[Int](arr) // searches for first Int and replaces it -> Array[Array[Array[Array[WidthT[Int]]]]]
val v3 = Counters[Int](arr) // searches for first Array[Int] and replaces it -> Array[Array[Array[Counters]]]

Postfix syntax (implemented only for tagged types)

import supertagged.postfix._

value @@ Width
value @@@ Width
value !@@ Width
value untag Width

Overtagged

Newtypes

Newtypes Basics

object Step extends NewType[Raw]{
    //...implicit scope for Step ...
    //...put here all implicits you need and they will be found here without additional imports...
    //...if you want to add more operations to Step, just define one more implicit class with ops...
    
    implicit final class Ops(private val value:Type) extends AnyVal {
        //... your methods here ...    
    }
    
    implicit def someCommonImplicit = ... // conversions, wrappers, typeclasess & etc...
}
type Step = Step.Type
object WidthT extends NewTypeT {
    type Raw[T] = T
}
type WidthT[T] = WidthT.Type[T]

val v1:WidthT[Int] = WidthT[Int](5)
val v2:WidthT[Long] = WidthT[Long](5L) 
object Counters extends TaggedType[T]{
  type Raw[T] = List[T]
}
type Counters[T] = Counters.Type[T]

val v1:Counters[Long] = Counters[Long](List(5L)) // You can't make newtype from Array[primitives], because it will fail at runtime with `can't cast` error
val v2:Counters[String] = Counters[String](List("String"))
val arr:List[List[List[List[Int]]]]
val v1 = Width(arr) // -> Array[Array[Array[Array[Width]]]]
val v2 = Width[Int](arr) // -> Array[Array[Array[Array[Width[Int]]]]]
val v3 = Counters[Int](arr) // -> Array[Array[Array[Counters]]]

Newtypes Custom

object Unfold extends NewType0 {

    protected type T[A,B] = A => Option[(A, B)]
    type Type[A,B] = Newtype[T[A,B],Ops[A,B]]
    
    
    implicit final class Ops[A,B](private val f: Type[A,B]) extends AnyVal {
      def apply(x: A): Stream[B] = raw(f)(x) match {
        case Some((y, e)) => e #:: apply(y)
        case None => Stream.empty
      }
    }
    
    def apply[A,B](f: T[A,B]):Type[A,B] = tag(f) // `tag` built in helper
    def raw[A,B](f:Type[A,B]):T[A,B] = cotag(f) // `cotag` built in helper
}
type Unfold[A,B] = Unfold.Type[A,B]

def digits(base:Int) = Unfold[Int,Int]{
    case 0 => None
    case x => Some((x / base, x % base))
}

digits(10)(123456).force.toString shouldEqual "Stream(6, 5, 4, 3, 2, 1)"

More examples

At: newtypes tests

Refined

object Meters extends TaggedType0[Long] {
    def apply(value:Long):Type = if(value >= 0) TaggedOps(this)(value) else throw new Exception("Can't be less then ZERO")
    
    def option(value:Long):Option[Type] = if(value >= 0) Some( TaggedOps(this)(value)) else None
}
type Meters = Meters.Type


Meters(-1) // will throw Exception
Meters(5) // would be `5:Meters`
Meters.option(-1) // would be `None`
Meters.option(0) // would be `Some(0:Meters)`

Lifting typeclasses for tagged & newtypes

You have several options:

1. Using LiftF for concrete F && all tagged types


import supertagged.@@
import supertagged.lift.LiftF
import io.circe.Encoder

implicit def lift_circeEncoder[T,U](implicit F:Encoder[T]):Encoder[T @@ U] = LiftF[Encoder].lift
implicit def lift_circeDecoder[T,U](implicit F:Decoder[T]):Decoder[T @@ U] = LiftF[Decoder].lift

2. Using LiftF for concrete F && concrete Tag

import supertagged.lift.LiftF

implicit val step_circeEncoder = LiftF[io.circe.Encoder].lift[Step.Raw, Step.Tag]
// -or-
implicit val step_circeEncoder:io.circe.Encoder[Step] = LiftF[io.circe.Encoder].lift

3. Using helper trait and mixing it when you need (works for TaggedType & NewType, can be adopted for more complex types)


trait LiftedCirce {
    type Raw
    type Type
    
    implicit def ordering(implicit origin:Ordering[Raw]):Ordering[Type] = unsafeCast(origin)
}

trait LiftedCirceT {
    type Raw[T]
    type Type[T]
    
    implicit def circeEncoder[T](implicit origin:io.circe.Encoder[Raw[T]]):io.circe.Encoder[Type[T]] = unsafeCast(origin)
    implicit def circeDecoder[T](implicit origin:io.circe.Decoder[Raw[T]]):io.circe.Decoder[Type[T]] = unsafeCast(origin)
}

object Step extends TaggedType[Int] with LiftedCirce
type Step = Step.Type

4. Using method @lift from TaggedType, NewType traits

object Step extends TaggedType[Int] {
  // (they will be used without additional imports)
  implicit def circeEncoder:io.circe.Encoder[Type] = lift
  implicit def circeDecoder:io.circe.Decoder[Type] = lift
}
type Step = Step.Type

Or in place:

object Step extends TaggedType[Int]
type Step = Step.Type


//somewhere else...
{
  import Step
  
  val liftedEncoder:io.circe.Encoder[Step] = Step.lift
  val liftedEncoder = Step.lift[io.circe.Encoder] // will be  io.circe.Encoder[Step]
}

5. Using liftAnyF [not recommended]

Will try auto lift of any requested typeclass Not recommended. Because of loosing control of what you are lifting.

import supertagged.lift.liftAnyF

callMethodWithImplicitTypeClass(step)

Unapply

val width = Width(5)

width match {
  case Width(5) => //...
}
val widthInt = WidthT[Int](5)
val EInt = WidthT.extractor[Int] // boiler plate, because Scala `match` don't support syntax with type parameters at now. Ex: `case EInt.extractor[Int](1)`

widthInt match {
  case EInt(1) => false
  case EInt(5) => true
}

Migration from 1.4 to 2.x

Specific Scalac BUG

Polymorphic expression cannot be instantiated

In versions before 2.0 was scalac specific bug in some very specific cases. Now it is absent.

Overriding + for Newtypes

  1. Everybody knows about existing autoimport Predef.scala. But it is not obvious that there is a weird implicit final class any2stringadd { def +(other: String)... }. Because of autoimport it used by compiler as direct scope for searching implicits. It means, that companion objects are not checked if he has found appropriate implicit in direct scope. So, he will use this any2stringadd.+ if you write x:MyNewType) + argument
  2. In further versions of Scala this class will be removed, but now you have few variants to overcome these:
    1. import Predef.{any2stringadd => _,_} - Shadowing
    2. import supertagged.newtypeOps - will force compiler to check companion object for newtype and prefer implicit ops from it.

Classic way

Original idea by Miles Sabin. Similar implementations are also available in shapeless and scalaz.

import supertagged.classic.@@

sealed trait Width
val value = @@[Width](5) // value is `Int @@ Width`

Alternatives (partially)