rudogma / scala-supertagged

Better (multi-nested-)tagged + newtypes types for Scala, Intellij Idea hints compatible 100%

GitHub

Build status Maven Central

supertagged for scala

Better (multi-nested-)tagged types for Scala, Intellij Idea autocomplete features working pretty fine.

Zero-dependcy 1 file, tests included.

sbt

Scala: 2.11.11, 2.12.1, 2.12.2

libraryDependencies += "org.rudogma" %% "supertagged" % "1.4"

ScalaJS (compiled with 0.6.17)

libraryDependencies += "org.rudogma" %%% "supertagged" % "1.4"

Usage

Check out tests for all examples

Classic way

Original idea by Miles Sabin. Similar implementations are also available in Shapeless and Scalaz.

import supertagged.@@

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

New way

Concepts and Features

Tagging Primitive, Class or any Trait types (and Multi Tagging). Original idea to use base trait + companion type is from Alexander Semenov https://github.com/Treev-io/tagged-types/

//
object Width extends TaggedType[Int]
type Width = Width.Type 

Overtagging. Original idea from Me. Heavily used in Scala Superquants https://github.com/Rudogma/scala-superquants

object Time extends TaggedType[Long]
type Time[T] = (Long with Tag[Long, Time.Tag]) @@ T

object Seconds extends OverTagged(Time)
type Seconds = Seconds.Type

Unified syntax

@@ - Adds one more tag to existing tags (if no tags then adds one)

!@@ - Replaces all existing tags with 1 new (if no tags then adds one)

untag - For removing concrete tag

Auto tagging at any nested level

No matter how many levels, it will stop automatically at appropriate (top level, middle or tail nested) (or fail if u used inappropriate types)

Widths @@ ( Width @@ Array(Array(Array(Array(Array(1,2,3)))))) // Result: `array_5lvl_OfWidth: Array[Array[Array[Array[Array[Int @@ Width] @@ Widths]]]]]`

Newtypes

Look into file for examples: TestNewTypes.scala

Tagging

FileWithModels.scala

import supertagged.TaggedType

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

object Widths extends TaggedType[Array[Width]]
type Widths = Widths.Type


// bounded
// Look at releases tab for notes on 1.4
// Look for examples in TestBoundedTaggedTypes.scala
import supertagged.{ TaggedTypeF, TaggedTypeFF }

object Post extends TaggedTypeF
type Post[T] = Post.Type[T]

object Widths extends TaggedTypeFF[Array]
type Widths[T] = Widths.Type[T]


Program.scala

// We don't need to import any from supertagged to use defined tags
import FileWithModels._

// U can use defined type `Width` without boilerplate `Int @@ Width`.
// Also all methods that waiting for raw Int are applicable for any tagged value based on Int,
// but method with `width:Width` will deny raw Int
def methodRaw(width:Int):Unit = {}
def method(width:Width):Unit = {}


val width = Width @@ 5  // or Width(5). Result: `width:Int @@ Width`

//Tagged values do not loose their raw types
methodRaw(width)
method(width)


val arrayOfWidth = Width @@ Array(1,2,3) // Result: `arrayOfWidth: Array[Int @@ Width]`

// No matter how many levels, it will stop automatically at appropriate (or fail if no)
val array_5lvl_OfWidth = Width @@ Array(Array(Array(Array(Array(1,2,3))))) // Result: `array_5lvl_OfWidth: Array[Array[Array[Array[Array[Int @@ Width]`

// `Widths @@ Array(1,2,3)` - will fail to compile, because Widths is `TaggedType[Array[Width]]` and we try to tag `Array[Int]`
val widths = Widths @@ arrayOfWidth // or Widths @@ (Width @@ Array(Array(Array(1,2,3))))  // Result: `widths: Array[Array[Array[Int @@ Width] @@ Widths]]`

// Any containers F[_]
val anyContainers = Width @@ List(Array(List(Array(1,2,3)))) // Result: `anyContainers: List[Array[List[Array[Int @@ Width]]]]`


// Bounded && plain. Combine them all
val offsetsInt = Offsets[Width] @@ (Width @@ Array(1,2,3)) // Result: `Array[Int @@ Width] @@ Offsets[Width]`

def testOffsets(offsets:Offsets[Width]):Unit = {}
//two methods with one name? Just add DummyImplicit(no imports required) and compiler will do the rest
def testOffsets(offsets:Offsets[Height])(implicit d:DummyImplicit):Unit = {}

MultiTagging

val value = Width @@ (Height @@ 5)) // Result: `Int @@ (Height with Width)`

takeWidth(value)
takeHeight(value)

def takeWidth(width:Width):Unit = {}
def takeHeight(height:Height):Unit = {}

val nested = Width @@ (Height @@ Array(Array(Array(5)))) // Result: `Array[Array[Array[Int @@ (Height with Width)]]]`

Postfix syntax

//required for postfix syntax!
import supertagged._

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

implicit Serializer case

Preparing

trait Serializer[T] {
    def serialize(t: T): String
}

def serialize[T](t: T)(implicit serializer: Serializer[T]): String = serializer.serialize(t)


implicit val longSerializer: Serializer[Long] = new Serializer[Long] {
def serialize(t: Long): String = "Long number: " + t
}


trait UserId

Example 1:

import supertagged._
implicit val lifter = lifterF[Serializer] // `import supertagged._` + `implicit val lifter` will auto lift all Serializer[T] to Serializer[T @@ WhatTagImplicitNeeds]

val longNumber = 30L
val id = tag[UserId](longNumber)

serialize(id) // `val longSerializer:Serializer[Long]` will be lifted to `Serializer[Long @@ UserId]`

Example 2:

import supertagged.liftAnyF // will lift any F[T] to F[T @@ WhatTagImplicitNeeds] when needed

val longNumber = 30L
val id = tag[UserId](longNumber)

serialize(id)

Ordering

Since TaggedType[T] already contains implicit def ordering[U](implicit origin:Ordering[T]):Ordering[T @@ U] = cast(origin), there is no need to import anything. Implicit Ordering[Raw] will be used implicitly for sorting tagged Raw @@ TaggedType[Raw].Tag.

import models.Counter

val arr = Counter @@ Array(3,10,1,2,11)
val arrSorted = arr.sorted

arrSorted.mkString(",") shouldBe "1,2,3,10,11" // Ok

Specific Scalac BUG

    /**
      * Be very attentive
      */

    {
      //Scalac Bug is here. It doesn't compile - and it is bug
      illTyped("""test(Counter @@ Array(1,2,3))""","polymorphic expression cannot be instantiated to expected type;.+")
      illTyped("""test(Counters @@ (Counter @@ Array(1,2,3)))""","polymorphic expression cannot be instantiated to expected type;.+")

    }

    {
      //We can overcome with using outer variable
      val v = Counter @@ Array(1,2,3)
      test(v) shouldBe 1
    }

    {
      //Or adding one more tag to parameter
      test2(Counters @@ (Counter @@ Array(1,2,3))) shouldBe 1
    }

    /**
      * Note: if change to List or adding inline conversion, or tagging nested array(if outer collection is not array) - everything compiles ok.
      */
    {

      testList(Counter @@ List(1,2,3)) shouldBe 1
      test( (Counter @@ List(1,2,3)).toArray ) shouldBe 1
      testNested( Counter @@ List(Array(1,2,3))) shouldBe 1
    }

  }

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

  object Counters extends TaggedType[Array[Counter]]
  type Counters = Counters.Type

  def test(counters:Array[Counter]):Counter = counters.head

  def test2(counters:Counters):Counter = counters.head
  def testNested(counters:List[Array[Counter]]):Counter = counters.head.head //(0) //see 'jvm feature block'

  def testList(counters:List[Counter]):Counter = counters.head