Shapeless 3

repository·main·Indexed 42 days ago

https://github.com/typelevel/shapeless-3

A generic programming library for Scala 3 based on type classes and dependent types. It provides improved performance and support for higher-kinded derivation, allowing automatic type class derivation for ADTs using the `derives` keyword. Key features include the `shapeless3-deriving` module for deriving Functor and FunctorK, utilities for retrieving type and field annotations, and the `Typeable` type class for type-safe runtime casting and pattern matching.

Tokens
2.9K
Snippets
12
Records
13
Agent score
51%

What's inside Shapeless 3

  1. Derive type classes for ADTs using `derives`

    main

    Shapeless 3 provides a generalized implementation of type class derivation. You can use the derives keyword on case classes or enums to automatically generate instances for type classes.

    Supported kinds include:

    • * (e.g., Monoid, Eq, Show)
    • * -> * (e.g., Functor, Traverse, Monad)
    • (* -> *) -> * (e.g., FunctorK / HFunctor)
    • * -> * -> * (e.g., Bifunctor)

    This approach is more efficient in terms of compile-time and runtime performance, and results in a smaller binary footprint compared to Shapeless 2.

    import shapeless3.deriving.*
    
    // Example: Deriving a Monoid for a case class
    trait Monoid[A]:
      def empty: A
      def combine(x: A, y: A): A
    
    // ... (Monoid implementation details) ...
    
    case class ISB(i: Int, s: String, b: Boolean) derives Monoid
    val a = ISB(23, "foo", true)
    val b = ISB(13, "bar", false)
    val c = a |+| b // == ISB(36, "foobar", true)
  2. Install shapeless3-deriving

    main

    To include the deriving module in your Scala 3 project, add the following dependency to your build file (e.g., build.sbt). Shapeless 3 is compatible with Scala 3.0.0 for the JVM and Scala.js 1.5.0+.

    libraryDependencies ++= Seq("org.typelevel" %% "shapeless3-deriving" % "3.4.0")
  3. Add shapeless-3 to your Scala project

    main

    To use shapeless-3 for generic programming in your Scala project, add the shapeless3-deriving module to your libraryDependencies in your build configuration (e.g., build.sbt). Replace @VERSION@ with the desired version of the library.

    libraryDependencies += "org.typelevel" %% "shapeless3-deriving" % "@VERSION@"
  4. Derive FunctorK for higher-order functors

    main

    Shapeless 3 supports deriving higher-order functors (like FunctorK). This allows you to map the effect within a data type parameterized with an effect.

    import shapeless3.deriving.*
    
    // An Option like type, with a default
    enum OptionD[T]:
      case Given(value: T)
      case Default(value: T)
    
      def fold: T = this match {
        case Given(t) => t
        case Default(t) => t
      }
    
    object OptionD:
      val fold: OptionD ~> Id = [t] => (ot: OptionD[t]) => ot.fold
    
    // A data type parameterized with an effect
    case class OrderF[F[_]](
      item: F[String],
      quantity: F[Int]
    ) derives FunctorK
    
    val incompleteOrder = OrderF(Given("Epoisse"), Default(1))
    val completeOrder = FunctorK[OrderF].mapK(incompleteOrder)(OptionD.fold)
    // == OrderF[Id]("Epoisse", 1)
  5. Derive Functor for enums

    main

    You can derive type classes for higher-kinded types like Functor for enums using the derives syntax.

    import shapeless3.deriving.*
    
    enum Opt[+A] derives Functor:
      case Sm[+A](value: A)
      case Nn
    
    Sm("foo").map(_.length) // == Sm(3)
  6. Retrieve all type annotations with AllTypeAnnotations[T]

    main

    The AllTypeAnnotations[T] type class provides a collection of all type annotations applied to the fields of a product type or constructors of a sum type T.

    apply() returns a Tuple where each element is a Tuple containing all type annotations for that field/constructor. If no type annotations are present, the inner tuple is EmptyTuple.

    case class First(s: String)
    case class Second(i: Int)
    case class CC(i: Int, s: String @First("a") @Second(0))
    
    val ccAllTypes = AllTypeAnnotations[CC]
    // ccAllTypes.Out is (EmptyTuple, (First, Second))
    // ccAllTypes.apply() is (EmptyTuple, (First("a"), Second(0)))
  7. Access an annotation on a type with Annotation[A, T]

    main

    Use the Annotation[A, T] type class to retrieve a specific annotation A applied to a type T. If the annotation exists, apply() returns the instance of the annotation.

    Note: The annotation type A must be a concrete class (not abstract or a trait) for this to work.

    case class First(i: Int)
    
    @First(3) trait Something
    
    val somethingFirst = Annotation[First, Something].apply()
    assert(somethingFirst == First(3))
  8. Perform type-safe casting with Typeable

    main

    The Typeable[T] type class provides utilities for performing type-safe runtime casting and type checking. It is most commonly used via extension methods provided in shapeless3.typeable.syntax.typeable.

    Key Operations

    • cast[U]: Attempts to cast the receiver to type U. Returns Some(value) if successful, or None if the cast is not possible.
    • castable[U]: Returns true if the receiver can be safely cast to type U, otherwise false.
    • narrowTo[U]: A specialized cast that requires U to be a subtype of the receiver's static type (U <:< T). It returns Option[U].

    To use these, ensure a Typeable[U] instance is in scope.

    import shapeless3.typeable.syntax.typeable.*
    
    val x: Any = 42
    
    // Using cast
    val maybeInt: Option[Int] = x.cast[Int]
    
    // Using castable
    val isString: Boolean = x.castable[String]
    
    // Using narrowTo (requires U <:< T)
    val y: Any = "hello"
    val narrow: Option[String] = y.narrowTo[String]
  9. Define custom Typeable instances

    main

    If the automatic derivation (via given [T]: Typeable[T]) does not cover your type, you can manually construct Typeable instances using several factory methods:

    • namedSimpleTypeable[T](clazz: Class[T], name: String): For simple classes where you want to check against a specific Class.
    • valueSingletonTypeable[T](value: T, name: String): For types that represent a specific constant value.
    • referenceSingletonTypeable[T](value: T, name: String, serializable: Boolean): For singleton reference types (e.g., object definitions).
    • intersectionTypeable[T](parents: Seq[Typeable[?]]): For intersection types (A & B), where the value must satisfy all Typeable conjuncts.
    • unionTypeable[T](parents: Seq[Typeable[?]]): For union types (A | B), where the value must satisfy at least one Typeable disjunct.
    • namedCaseClassTypeable[T](clazz: Class[T], fields: Seq[Typeable[?]], name: String): For case classes, checking both the class identity and the types of all its fields.
    • namedSumTypeable[T](elems: Seq[Typeable[?]], name: String): For sum types (sealed traits), checking if the value matches any of the provided element types.
    • recursive[T](f: Typeable[T] => Typeable[T]): For recursive types, allowing you to tie the knot using a lazy evaluation pattern.
  10. Retrieve type annotations with TypeAnnotations[A, T]

    main

    The TypeAnnotations[A, T] type class inspects the types of fields in a product type or constructors in a sum type T for annotations of type A.

    apply() returns a Tuple where each element is None.type or Some[A]. This is distinct from Annotations because it looks at the type position (e.g., s: String @First("a")) rather than the term position.

    Note: Type annotations must not be abstract.

    case class First(s: String)
    case class CC(i: Int, s: String @First("a"))
    
    val ccFirsts = TypeAnnotations[First, CC]
    // ccFirsts.Out is (None.type, Some[First])
    // ccFirsts.apply() is (None, Some(First("a")))
  11. Retrieve all variable annotations with AllAnnotations[T]

    main

    The AllAnnotations[T] type class provides a collection of all variable annotations applied to the fields of a product type or constructors of a sum type T.

    apply() returns a Tuple where each element is another Tuple containing all annotations for that specific field/constructor. If no annotations are present, the inner tuple is EmptyTuple.

    case class First(s: String)
    case class Second(i: Int)
    case class CC(i: Int, @First("a") @Second(0) s: String)
    
    val ccAll = AllAnnotations[CC]
    // ccAll.Out is (EmptyTuple, (First, Second))
    // ccAll.apply() is (EmptyTuple, (First("a"), Second(0)))
  12. Retrieve field or constructor annotations with Annotations[A, T]

    main

    The Annotations[A, T] type class inspects the fields of a product type or the constructors of a sum type T to find annotations of type A.

    apply() returns a Tuple of type Out, where each element is either None.type (if no annotation was found) or Some[A] (the annotation instance).

    Note: Annotation types must be concrete.

    case class First(s: String)
    case class CC(i: Int, @First("a") s: String)
    
    sealed trait Base
    @First("b") case class BaseI(i: Int) extends Base
    @First("c") case class BaseS(s: String) extends Base
    
    val ccFirsts = Annotations[First, CC]
    // ccFirsts.Out is (None.type, Some[First], None.type)
    // ccFirsts.apply() is (None, Some(First("a")), None)
    
    val baseFirsts = Annotations[First, Base]
    // baseFirsts.Out is (Some[First], Some[First])
    // baseFirsts.apply() is (Some(First("b")), Some(First("c")))