Giter Site home page Giter Site logo

union-derivation's Introduction

union-derivation

Continuous Integration union-derivation-core Scala version support

A micro-library to derive a typeclass for Scala 3 Union types.

Getting started

To use union-derivation in an existing SBT project with Scala 3.3.1 or a later version, add the following configuration to your build.sbt:

libraryDependencies += "io.github.irevive" %% "union-derivation-core" % "0.1.0"
scalacOptions += "-Yretain-trees" // important for the detection of an abstract method in a trait

Versions matrix:

Scala Library JVM Scala Native Scala.js
3.1.2 0.0.3 + - -
3.2.0+ 0.0.4+ + + -
3.3.1+ 0.1.0+ + + +

Usage example

Typeclass definition

import io.github.irevive.union.derivation.{IsUnion, UnionDerivation}

import scala.compiletime.{erasedValue, summonInline}
import scala.deriving.*

trait Show[A] {
  def show(a: A): String
}

object Show extends ShowLowPriority {

  given Show[Int]    = v => s"Int($v)"
  given Show[Long]   = v => s"Long($v)"
  given Show[String] = v => s"String($v)"

  inline given derived[A](using m: Mirror.Of[A]): Show[A] = { // (1)
    val elemInstances = summonAll[m.MirroredElemTypes]
    inline m match {
      case s: Mirror.SumOf[A]     => showSum(s, elemInstances)
      case _: Mirror.ProductOf[A] => showProduct(elemInstances)
    }
  }

  inline def summonAll[A <: Tuple]: List[Show[?]] =
    inline erasedValue[A] match {
      case _: EmptyTuple => Nil
      case _: (t *: ts)  => summonInline[Show[t]] :: summonAll[ts]
    }

  private def showA[A](a: A, show: Show[?]): String = 
    show.asInstanceOf[Show[A]].show(a)

  private def showSum[A](s: Mirror.SumOf[A], elems: => List[Show[?]]): Show[A] =
    new Show[A] {
      def show(a: A): String = showA(a, elems(s.ordinal(a)))
    }

  private def showProduct[A](elems: => List[Show[?]]): Show[A] = 
    new Show[A] {
      def show(a: A): String = {
        val product = a.asInstanceOf[Product]

        product.productIterator
          .zip(product.productElementNames)
          .zip(elems.iterator)
          .map { case ((field, name), show) => s"$name = ${showA[Any](field, show)}" }
          .mkString(product.productPrefix + "(", ", ", ")")
      }
    }

}

trait ShowLowPriority {
  inline given derivedUnion[A](using IsUnion[A]): Show[A] = UnionDerivation.derive[Show, A] // (2)
}
  1. The derivation mechanism. Checkout Scala 3 docs for more details.
  2. derivedUnion has IsUnion constraint, therefore the method can be applied only to Union types.

Usage

type UnionType = Int | Long | String
final case class User(name: String, age: Long, flags: UnionType)

val unionShow: Show[UnionType] = summon[Show[UnionType]]
// unionShow: Show[UnionType] = repl.MdocSession$MdocApp$$Lambda$16196/0x00000070037ccd80@4475cd21
val userShow: Show[User] = summon[Show[User]]
// userShow: Show[User] = repl.MdocSession$$anon$9@5e969cb4

println(unionShow.show(1))
// Int(1)
println(unionShow.show(2L))
// Long(2)
println(unionShow.show("3"))
// String(3)
println(userShow.show(User("Pablo", 22, 12L)))
// User(name = String(Pablo), age = Long(22), flags = Long(12))
println(userShow.show(User("Pablo", 33, 1)))
// User(name = String(Pablo), age = Long(33), flags = Int(1))

Generated code

The library creates a set of if-else statements for the known types of the union.

The simplified version of the generated code:

val instance: Show[Int | String | Long] = UnionDerivation.derive[Show, Int | String | Long]

// expands into
val instance: Show[Int | String | Long] = { (value: Int | String | Long) =>
  if (value.isInstanceOf[Int]) summon[Show[Int]].show(value)
  else if (value.isInstanceOf[String]) summon[Show[String]].show(value)
  else if (value.isInstanceOf[Long]) summon[Show[Long]].show(value)
  else sys.error("Impossible")
}

scala-cli

The library works out of the box with scala-cli too.

//> using scala "3.3.1"
//> using lib "io.github.irevive::union-derivation-core:0.1.0"
//> using options "-Yretain-trees"

import io.github.irevive.union.derivation.{IsUnion, UnionDerivation}

trait Show[A] {
  def show(value: A): String
}

given Show[String] = value => s"str: $value"
given Show[Int]    = value => s"int: $value"

inline given derivedUnion[A](using IsUnion[A]): Show[A] = UnionDerivation.derive[Show, A]

println(summon[Show[String | Int]].show(1))
// int: 1
println(summon[Show[String | Int]].show("1"))
// str: 1

union-derivation's People

Contributors

dependabot[bot] avatar github-actions[bot] avatar irevive avatar majk-p avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

majk-p

union-derivation's Issues

Please consider upstreaming to Scala3

Hi, thanks for the awesome library! I'm using heaps of unions so this is a lifesaver. Would you consider upstreaming this to Scala3? I think this is a missing feature that really belongs in the standard lib.

Add more examples

  • The current example is complex. The scala-cli example is much better for the showcase
  • Introduce two examples: minimal and full

Support concrete members

This code works as expected:

//> using scala "3.2.0"
//> using lib "io.github.irevive::union-derivation-core:0.0.3"

import io.github.irevive.union.derivation.{IsUnion, UnionDerivation}

trait Show[A]:

  def showValue(value: A): String

  //def show(value: A): String = showValue(value)

given Show[String] with

  override def showValue(value: String): String = s"str: $value"

given Show[Int] with

  override def showValue(value: Int): String = s"int: $value"

inline given derivedUnion[A](using IsUnion[A]): Show[A] = UnionDerivation.derive[Show, A]

println(summon[Show[String | Int]].showValue(1)) //int: 1
println(summon[Show[String | Int]].showValue("1")) //str: 1

but when uncommenting the concrete method, the compiler throws the following error:

[error] ./union.sc:23:35: More than one abstract method detected in trait Show: showValue, show. Automatic derivation is impossible
[error] println(summon[Show[String | Int]].showValue("1"))
[error]                                   ^
[error] ./union.sc:22:35: More than one abstract method detected in trait Show: showValue, show. Automatic derivation is impossible
[error] println(summon[Show[String | Int]].showValue(1))
[error]                                   ^

I expect the code to still compile since the show member is concrete and shouldn't be redefined. Maybe an annotation would help to disambiguate and choose the method to reimplement?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.