Simple, fast and transparant generic derivation for typeclasses
Wisteria is a generic macro for automatic materialization of typeclasses for datatypes composed from product types (e.g. case classes) and coproduct types (e.g. enums). It supports recursively-defined datatypes out-of-the-box, and incurs no significant time-penalty during compilation.
- derives typeclasses for case classes, case objects, sealed traits and enumerations
- offers a lightweight but typesafe syntax for writing derivations avoiding complex macro code
- builds upon Scala 3's built-in generic derivation
- works with recursive and mutually-recursive definitions
- supports parameterized ADTs (GADTs), including those in recursive types
- supports both consumer and producer typeclass interfaces
- fast at compiletime
- generates performant runtime code, without unnecessary runtime allocations
Wisteria 0.3.0 is available as a binary for Scala 3.4.0 and later, from Maven
Central. To include it in an sbt
build, use
the coordinates:
libraryDependencies += "dev.soundness" % "wisteria-core" % "0.3.0"
Wisteria makes it easy to derive typeclass instances for product and sum types, by defining the rules for composition and delegation as simply as possible.
This is called generic derivation, and given a typeclass which provides some functionality on a type, it makes it possible to automatically extend that typeclass's functionality to all product types, so long as it is available for each of the product's fields; and optionally, to extend that typeclass's functionality to all sum types, so long as it is available for each of the sum's variants.
In other words, if we know how to do something to each field in a product, then we can do the same thing to the product itself; or if we can do something to each variant of a sum, then we can do the same thing to the sum itself.
In this documentation, and in Wisteria, we use the term product for types which are composed of a specific sequence of zero or more values of other types. Products include case classes, enumeration cases, tuples and singleton types, and the values from which they are composed are called fields. The fields for any given product have fixed types, appear in a canonical order and are labelled, though for tuples, the labels only indicate the field's position. Singletons have no fields.
Likewise, we use the term sum for types which represent a single choice from a specific and fixed set of disjoint types. Sum types include enumerations and sealed traits. Each of the disjoint types that together form a sum type is called a variant of the sum.
From a category-theoretical perspective, products and sums are each others' duals, and thus fields and variants are duals.
In the following example,
sealed trait Temporal
enum Month:
case Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec
case class Date(day: Int, month: Month, year: Int) extends Temporal
case class Time(hour: Int, minute: Int)
case class DateTime(date: Date, time: Time) extends Temporal
we can say the following:
Temporal
is a sum typeDate
andDateTime
are variants ofTemporal
Date
,Time
andDateTime
are all product typesday
,month
andyear
are fields ofDate
hour
andminute
are fields ofTime
date
andtime
are fields ofDateTime
Month
is a sum typeJan
through toDec
are all product types, all singletons, and all variants ofMonth
- the type,
(Month, Int)
(representing a month and a year) would be a product type, and a tuple
A typeclass is a type (usually defined as a trait), whose instances provide
some functionality, through different implementations of an abstract method on
the typeclass, corresponding to different types which are specified in one of
the typeclass's type parameters. Instances are provided as contextual values
(given
s), requested when needed through using
parameters, and resolved
through contextual search (implicit search) at the callsite.
Where necessary, we distinguish clearly between a typeclass interface (the
generic trait and abstract method) and a typeclass instance (a given
definition which implements the aforesaid trait). The term typeclass alone
refers to the typeclass interface.
The exact structure of a typeclass interface varies greatly, but typically, a typeclass is a trait, with a single type parameter, and a single abstract method, where the type parameter appears either in the method's return type or in one or more of its parameters.
We call typeclasses whose type parameter appears in their abstract method's return type producers, because they produce new instances of the parameter type. Typeclasses whose type parameter appears in their abstract method's parameters, consumers because existing instances of the parameter type are given to them. (The term consumer shouldn't be misinterpreted to imply that any value is "used up" in applying the typeclass's functionality; it will be passed into a method, but will continue to exist for as long as references to it continue to exist.)
Producers may be covariant (indicated by a +
before their type
parameter), and consumers may be contravariant (indicated by a -
before their
type parameter). But either can be defined as invariant.
For example,
trait Size[ValueType]:
def size(value: ValueType): Double
is an invariant consumer typeclass interface for getting a representation (as a
double) of the size of an instance of ValueType
. It might have instances
defined as:
object Size:
given Size[Boolean] = new Size[Boolean]:
def size(value: Boolean): Double = 1.0
given Size[Char]:
def size(value: Char): Double = 2.0
given Size[String] = _.length.toDouble
and even,
given [ElementType](using size: Size[ElementType]): Size[List[ElementType]] =
_.map(size.size(_)).sum
which constructs new typeclass instances for List
s on-demand, and which
requires a typeclass instance corresponding to the type of the List
's
elements. Since Size
is a single-abstract-method (SAM) type, it can be
implemented as a simple lambda corresponding to the abstract method.
Another typeclass example would be,
trait Default[+ValueType]:
def apply(): ValueType
which is a covariant producer typeclass interface.
Wisteria lets us say, for a particular typeclass interface but for any product type, "if we have instances of the typeclass available for every field, then we can construct a typeclass instance for that product type", and provides the means to specify how they should be combined.
Dually, we can say that, for a particular typeclass instance but for any sum type, "if we have instances of the typeclass available for every variant of the sum, then we can construct a typeclass instance for that sum type", and provides the means to specify how the instances should be combined.
Naturally, fields and variants may themselves be products or sums, so generic derivation may be applied recursively.
Hence, if we define all our datatypes out of products and sum types of "simple" types, then for a particular typeclass interface, we can define typeclass instances for the simple types plus a generic derivation mechanism, and typeclass instances will effectively be available for every datatype.
Generic derivation for sum types is not always needed or even desirable, so we will start by exploring product derivation.
A typical example of a consumer typeclass is the Show
typeclass. It provides
the functionality to take a value, and produce a string representation of that
value, and could be defined as,
trait Show[ValueType]:
def show(value: ValueType): Text
with an extension method to make it easier to apply the typeclass:
extension [ValueType: Show](value: ValueType)
def show: Text = summon[Show[ValueType]].show(value)
Generalizing over all products (and hence, all possible field types), our task is to define how a product type should be shown, if we're provided with the means to show each of its fields.
So, if we have Show
instances for Int
s and Text
s, then we want to be able
to derive a Show
instance for a type such as:
case class Person(name: Text, age: Int)
However, in the general case, we do not know how many fields there will be or what their types are, so we cannot rely on any of these details in our generic derivation definition.
To use Wisteria, we need to import the wisteria
package,
import wisteria.*
and add the ProductDerivation
trait to the companion object of the type we
want to define generic derivation for, along with the stub for the join
method, like so:
object Show extends ProductDerivation[Show]:
inline def join[DerivationType <: Product: ProductReflection]: Show[DerivationType] = ???
The signature of join
must be defined exactly like this:
- it must be
inline
- its type parameter must be a subtype of
Product
- it must have a context bound on
ProductReflection
- its return type must be an instance of the typeclass, parameterized on the method's type parameter
Given the return type, we know that we need to construct a new
Show[DerivationType]
instance, so we can start with the definition,
object Show extends ProductDerivation[Show]:
inline def join[DerivationType <: Product: ProductReflection]: Show[DerivationType] =
new Show[DerivationType]:
def show(value: DerivationType): Text = ???
We will implement show
by calling the method fields
, which is available as
a protected method inside ProductDerivation
, and which allows us to map over
each field in the product to produce an array of values, by means of a
polymorphic lambda. fields
also takes an instance of the product type, so it
can provide the actual field value from the product inside the lambda.
Here's what a call to fields
looks like:
object Show extends ProductDerivation[Show]:
inline def join[DerivationType <: Product: ProductReflection]
: Show[DerivationType] =
new Show[DerivationType]:
def show(value: DerivationType): Text =
val array: IArray[Nothing] = fields(value):
[FieldType] => field =>
???
???
The polymorphic lambda may be unfamiliar syntax, but it can be thought of as equivalent to as a lambda equivalent of a polymorphic method. So if the lambda for,
def transform(field: Field): Text
is, Field => Text
, then the lambda for,
def transform[FieldType](field: FieldType): Text
is, [FieldType] => FieldType => Text
.
This is necessary because each field will potentially have a different type,
but in the context of the fields
method, we know nothing about what these
types are, but it is useful to be able to name the type. The lambda variable,
field
, has the type FieldType
.
Although we can refer to field
's type as FieldType
in the lambda body, we
still have almost no information at all about the properties of this type. The
one thing we can assert, however, is that another occurrence of FieldType
is
at least referring to the same type.
Therefore, an instance of Show[FieldType]
, regardless of where it comes from,
will be able to show an instance of FieldType
.
By default, Wisteria will make just such an instance available contextually within the lambda body.
[FieldType] => field =>
summon[Show[FieldType]].show(field)
So, for each field this lambda is invoked on, a Show[Int]
, Show[Text]
or
Show[Person]
(or whatever type necessary) is summoned and supplied to it
contextually as a Show[FieldType]
. It's also available contextually by name
as `context, so we can also write,
[FieldType] => field =>
context.show(field)
but since it's contextual we can use the extension method above, and so it is
sufficient to write, [FieldType] => field => field.show
. ```
This gives us enough to construct an array of Text
values corresponding to
each field in a product, which we can join together to surround the :
object Show extends ProductDerivation[Show]:
inline def join[DerivationType <: Product: ProductReflection]
: Show[DerivationType] =
new Show[DerivationType]:
def show(value: DerivationType): Text =
val array: IArray[Text] = fields(value):
[FieldType] => field =>
field.show
array.join(t"[", t", ", t"]")
This definiton is sufficient to generate new (and working) contextual instances
of Show
for product types. Given the definition of Person
above,
Person(t"George", 19).show
would produce the string, [George, 19]
.
Similar to the fields
method, another method, contexts
, is provided for accessing the typeclasses
corresponding to each field, without using a preexisting instance of the derivation type for dereferencing.
This is close to what we need, but we would also like to include the type name.
This is available as a protected method of ProductDerivation
called,
typeName
, so we can adjust the last line to, array.join(t"$typeName[", t", ", t"]")
, and our new derivation will produce the string, Person[George, 19]
.
But we can go further. The name of each field can also be included in the
string output. The value label
is provided as a named contextual value inside
fields
's lambda, so we can access the label for any field from within the
lambda. Changing the definition to,
[FieldsType] => field =>
t"$label:${field.show}"
will change the output to Person[name:George, age:19]
.
We might also like to provide different behavior for certain kinds of product type; singletons and tuples. Singletons have no fields, so the brackets could be omitted for these products. And tuples' names are not so meaningful, so these could be omitted.
Two methods returning boolean values, singleton
and tuple
can be used to
determine whether the current product type is a singleton or a tuple. The
implementation of join
can be adapted to provide different strings in these
cases.
Since Show
is a SAM type, we can also simplify the implementation and write
the implementation of join
as a lambda. A full implementation would look like
this:
object Show extends ProductDerivation[Show]:
inline def join[DerivationType <: Product: ProductReflection]
: Show[DerivationType] =
value =>
if singleton then typeName else
fields(value):
[FieldType] => field => if tuple then field.show else t"$label=$field"
.join(if tuple then t"[" else t"$typeName[", t", ", t"]")
Some typeclasses operate on two values of the same type. An example is the Eq
typeclass for determining structural equality of two values:
trait Eq[ValueType]:
def equal(left: ValueType, right: ValueType): Boolean
When defining the join
method for Eq
, we could use the fields
method to
map over the fields of either left
or right
, but not both.
One solution would be to construct arrays of the field values of left
, the
field values of right
and the Eq
typeclasses corresponding to each field.
(Although the field values, and hence their corresponding typeclass instances
will be different from each other, the types of the elements of the left and
right arrays will at least be pairwise-compatible.) We could then iterate over
the three arrays together, applying the each typeclass to its corresponding
left and right field value, and then aggregating the results.
While possible, this would be inefficient and would rquire a significant
compromise of typesafety: inside the lambda, a value and a typeclass will be
typed according to FieldType
, and therefore uniquely compatible with each
other. But as soon as they are aggregated into an array, independent of each
other, their types would become incompatible, erased to Any
or Nothing
, and
could only be combined with explicit asInstanceOf
casts.
Wisteria avoids this by making it possible, within the fields
lambda of one
product value, to access the field value, from another product value, which
corresponds to the field in the current lambda, using the complement
method,
and to provide it with the same type so that it is compatible with that field's
contextual typeclass instance.
Here's a full implementation of Eq
:
object Eq extends ProductDerivation[Eq]:
inline def join[DerivationType <: Product: ProductReflection]: Eq[DerivationType] =
(left, right) =>
fields(left):
[FieldType] => leftField =>
context.equal(leftField, complement(right))
.foldLeft(true)(_ && _)
Producer typeclasses can also be generically derived. Wheras a consumer typeclass will receive a pre-existing instance of the derivation type as input, and produce a value of some invariant type, a producer typeclass will take an invariant type as input, and will construct a new instance of the derivation type.
An example of a producer typeclass would be a simple Random
typeclass which takes a long "seed" value as input
and constructs a random new instance from that seed. A Random
instance for a generic product type should
produce a new product instance, all of whose field values are chosen randomly.
Here is the definition of Random
:
trait Random[+ValueType]:
def next(seed: Long): ValueType
For a producer typeclass derivation, The join
signature will be identical, but instead of the fields
method,
we will need to use the construct
method to construct a new instance, without taking an existing instance of
the product type as input. A call to fields
will also take a polymorphic lambda specifying the field type, but
since we have no preexisting instance, and therefore no fields, its lambda variable is a reference to the
typeclass instance which can be used to instantiate the new field value.
object Random extends ProductDerivation[Random]:
inline def join[DerivationType <: Product: ProductReflection]: Random[DerivationType] = seed =>
construct:
[FieldType] => random =>
???
In fact, since we know nothing about the type of the field in the context of the lambda (except that we have a name for it), the typeclass instance, which shares the same type in its parameter, is our only means of constructing a new instance for that field.
Therefore, by parametricity, the only sensible way to implement the method is to invoke the next
method, like
so:
object Random extends ProductDerivation[Random]:
inline def join[DerivationType <: Product: ProductReflection]: Random[DerivationType] = seed =>
construct:
[FieldType] => random => random.next(seed)
Calling constuct
, specifying how each field's value will be computed, will return a new instance of the
product, DerivationType
. Since Random
is a SAM type, this expression of Long => DerivationType
provides
a suitable implementation for the new typeclass.
Often your producer will return a type construct, like Option
or Either
,
for example:
trait Parser[T]:
def parse(input: String): Either[Exception, T]
In this case there is a method called constructWith
, which can be used in
place of construct
, and allows you to specify polymorphic pure
and bind
(a.k.a. flatMap
) functions over your type constructor to help traverse
producer typeclass results.
Here is an example usage:
syntax scala
highlight [InputType..flatMap This is a polymorphic `bind` function
highlight [Monadic..(_) This is a polymorphic `pure` function
##
object Parser extends ProductDerivation[Parser]:
inline def join[DerivationType <: Product: ProductReflection]: Parser[DerivationType] = input =>
constructWith[DerivationType, Either]
([InputType, OutputType] => _.flatMap,
[MonadicType] => Right(_),
[FieldType] => context => context.parse(input))
Deriving sums, or coproducts, is possible by making a choice of which of their variants is represented by the
sum type. Deriving sums may be omitted for many typeclasses, since it's not as commonly useful as deriving
products. But if it is desired in addition to product derivation, a typeclass's companion object will need to
extend Derivation
instead of ProductDerivation
, and define an additional split
method.
Here are the adjusted stub implementations for the Show
typeclass:
object Show extends Derivation[Show]:
inline def join[DerivationType <: Product: ProductReflection]
: Show[DerivationType] = ???
inline def split[DerivationType: SumReflection]
: Show[DerivationType] = ???
Note that split
's signature is similar to join
's, but lacks the subtype constraint on DerivationType
and
uses a SumReflection[DerivationType]
instead of a ProductReflection
. An implementation of split
will have
some similarities with a join
implementation, but will use variant
and delegate
methods instead of
fields
and construct
.
To show an instance of a typeclass, we will use the variant
method to inspect a preexisting instance of the
derivation type and apply a lambda to the one variant which matches. This is a dual of the fields
method for
sum types, but unlike fields
the lambda will apply only to the matching variant; not to every variant.
Like fields
, though, we have no greater knowledge about the type of that variant in the context of the lambda,
so once again, we will specify a polymorphic lambda which takes a VariantType
type parameter. We do, however,
have one more piece of useful information about VariantType
which we didn't know about a field's type:
VariantType
must be a subtype of the derivation type. Therefore, we specify the lambda type variable as
[VariantType <: DerivationType]
:
inline def split[DerivationType: SumReflection]
: Show[DerivationType] =
value =>
variant(value):
[VariantType <: DerivationType] => variant =>
???
So, in the body of the variant
lambda, we now have an instance of VariantType
, which we know to be a subtype
of DerivationType
. This is actually exactly the same value as value
, but its type has been refined—to a type
which is more precise; but also abstract.
As was the case with fields
's lambda, we have some additional context available in this lambda: context
is an instance of Show[VariantType]
and label
is the name of the variant.
A trivial implementation of this lambda would just call variant.show
, since the contextual Show[VariantType]
value is available.
inline def split[DerivationType: SumReflection]
: Show[DerivationType] =
value =>
variant(value):
[VariantType <: DerivationType] => variant => variant.show
When we provided the product derivation for Eq
, we used the complement
method to get the corresponding field
with the correct type inside the body of fields
. The same is possible inside the body of variant
, but it
returns an Optional
value, since an unrelated value of the same sum type is, by no means, guaranteed to be
the same variant: if the other value is a different variant, then it would not make sense to resolve that
value with the same type—and so an Unset
value is returned from complement
.
If, however, both values represent the same variant, then we can access that value, safely typed with the same type.
Here is an implementation of split
for Eq
:
inline def split[DerivationType: SumReflection]
: Eq[DerivationType] =
(left, right) =>
variant(left):
[VariantType <: DerivationType] => leftValue =>
complement(right).let(context.equal(leftValue, _)).or(false)
The interpretation of this implementation is that if the left and right sum types represent the same variant,
then we use context
, the typeclass instance that is common to both, to compare them. Otherwise, since they are
evidently different, we return false
.
Therefore, a complete implementation of Eq
is as simple as:
trait Eq[ValueType]:
def equal(left: ValueType, right: ValueType): Boolean
object Eq extends Derivation[Eq]:
inline def join[DerivationType <: Product: ProductReflection]: Eq[DerivationType] =
(left, right) =>
fields(left):
[FieldType] => left => context.equal(left, complement(right))
.foldLeft(true)(_ && _)
inline def split[DerivationType: SumReflection]: Eq[DerivationType] =
(left, right) =>
variant(left):
[VariantType <: DerivationType] => left =>
complement(right).let(context.equal(left, _)).or(false)
As with the construct
method for product types, the delegate
method is used for producer sum types which
must return a new instance of the derivation type, without having a preexisting value to work with. While
variant
can unambiguously resolve which of the variants its parameter value represents, just from its runtime
type, the method of discerning which variant is required from its input will depend on the type of that input,
and is not guaranteed to succeed.
Imagine defining a Decoder
type which reads values from strings, and we expect the variant's type to be
encoded at the start of the string, for example, "Developer:Hamza,39"
and "Manager:Jane,52,2"
could both be
representations of instances of the sum type:
enum Employee:
case Developer(name: Text, age: Int)
case Manager(name: Text, age: Int, level: Int)
We would like to inspect the part of the string before the :
and delegate to either the Developer
or
Manager
variants accordingly.
But the typeclass could be passed the string, "Director:Beatrice,47"
, and no variant would exist in the
Employee
sum type to delegate to.
As its first parameter, delegate
expects the name of the variant (i.e. its label
value) to delegate to. Its
second parameter is another polymorphic lambda. As with construct
which had no field
lambda variable,
delegate
has no variant
lambda variable, and (likewise) offers the matching variant's context.
For our Decoder
example, we have:
object Decoder extends Derivation[Decoder]:
inline def split[DerivationType: SumReflection]
: Decoder[DerivationType] =
text =>
val prefix = text.cut(t":").head
delegate(prefix):
[VariantType <: DerivationType] => decoder =>
???
Having discerned which variant's decoder should be used, we can then use this to decode the text following the
:
, like so:
object Decoder extends Derivation[Decoder]:
inline def split[DerivationType: SumReflection]
: Decoder[DerivationType] =
text =>
text.cut(t":") match
case List(prefix, content) => delegate(prefix):
[VariantType <: DerivationType] => decoder =>
decoder.decode(content)
Sometimes it is useful to derive a typeclass only for enums of singleton variants, such as,
enum Country:
case De, Fr, Gb
but not for enumerations with one or more structural cases such as:
syntax scala
highlight En..ct) English has multiple dialects
highlight Eo Esperato has no dialects; it is a singleton
##
enum Language:
case En(dialect: Dialect)
case Eo
The allSingletons
method returns true
if every case in a sum type is a
singleton. This also applies to sealed traits of case objects.
Here is an example of its use deriving Show
:
trait Show[ValueType]:
def show(value: ValueType): String
object Show extends Derivation[Show]:
inline def join[DerivationType <: Product: ProductReflection]
: Show[DerivationType] =
value => ???
inline def split[DerivationType: SumReflection]
: Show[DerivationType] =
value =>
inline if !allSingletons then compiletime.error("cannot derive") else
variant(value): [VariantType <: DerivationType] =>
variant => typeName.s+"."+variant.show
Note that inline if
is used to ensure that allSingletons
is evaluated at
compiletime, enabling the error branch (compiletime.error
) to be retained
or eliminated. If it is retained, compilation will fail.
By default, derivation will fail at compiletime if a field's or variant's corresponding typeclass instance cannot be found by contextual search. This is usually the desired behavior because it indicates the absence of definitions which are inherently necessary.
But it's not unusual to want generic derivation to succeed, accepting that we should provide a fallback option
when a contextual value is not found. This can be achieved by importing derivationContext.relaxed
in the scope
where join
and split
are defined.
The presence of this import will change the signature of methods such as fields
slightly, so that the
contextual value provided to its lambda is an Optional[Typeclass]
instead of a Typeclass
instance. This
means that there will no longer be a contextual Typeclass
available, so any calls which expect one will fail
to compile, but there will be a contextual Optional[Typeclass]
value instead, and various control methods on
Optional
values can be used to work with such a type.
We could take the Show
example from earlier and adjust it to fall back to a field's toString
value if a
Show
typeclass does not exist for that type:
object Show extends ProductDerivation[Show]:
inline def join[DerivationType <: Product: ProductReflection]
: Show[DerivationType] =
value =>
fields(value):
[FieldType] => field => context.layGiven(field.toString.tt)(field.show)
.join(t"[", t", ", t"]")
This adjusted version refers to the contextual Show[FieldType]
value, which is available as context
inside
the lambda, and uses layGiven
to provide the fallback option in the first parameter block, with the original
code (for when the typeclass is available) in the second block. This is made possible because when the
Optional
value is present, layGiven
injects its value contextually into this parameter block.
Case classes may be defined with default values for some of their fields. These default values, if available, can be useful during derivation. As one example, a JSON or XML decoder may construct a product instance from values provided at runtime, but could choose to use that product's default field values whenever a field's value is missing from the runtime input.
A contextual Default[Optional[FieldType]]
instance called default
is available within the lambda body of
fields
and contexts
, and calling default()
within either of these contexts will provide an
Optional[FieldType]
.
How can I avoid generic derivation failing when a typeclass for one or more parameters is missing?
Include the import,
import wisteria.derivationContext.relaxed
in the context where join
and split
are defined. This will transform the type of the typeclass value
corresponding to the field from TypeclassType[ValueType]
to Optional[TypeclassType[ValueType]]
. Normally,
this also means that the typeclass will need to be applied explicitly.
How can I use other unrelated typeclasses in a join
or split
implementation?
The signatures of join
and split
cannot be changed, so it is impossible to include other typeclass instances
in their implementations. But both are inline methods, so summonInline
and summonFrom
can be used to summon
instances of other typeclasses at compiletime, whether these relate to the derivation type or a field type.
How can I use Wisteria for generic derivation without making the generically-derived typeclasses available to implicit search?
Use a non-companion object extending Derivation
or ProductDerivation
for the definitions of join
and
split
, and call the inline derived
method on that object, passing in the derivation type.
Why is a generically-derived typeclass instance not being found when it is summoned?
This is usually because typeclass instances relating to one or more field or variant values cannot be found. To
test this theory, try compiling an explicit call to the inline derived
method at the callsite where
contextual search is failing.
Why is another contextual instance being selected by contextual search instead of a generically-derived one?
Assuming the generically-derived typeclass instance is a valid candidate for selection, this is probably
because the derived candidate has a lower priority. Since the given
instance is defined in either
ProductDerivation
or Derivation
, which is typically inherited by the typeclass's companion object, its
priority is naturally lower than given
instances defined in the body of that companion object.
One solution would be to artificially reduce the priority of the undesired contextual instances, for example by
adding an additional (using DummyImplicit)
parameter, or moving the definition to an inherited trait.
Another solution is to define join
and split
in an unrelated (non-companion) object, and to define an inline
given called derived
directly in the companion object, like so:
object Unrelated extends ProductDerivation[Typeclass]:
def join[DerivationType <: Product: ProductReflection]
: Typeclass[DerivationType] = ???
object Typeclass:
inline given derived[DerivationType]: Typeclass[DerivationType] =
Unrelated.derived
- How can I resolve a derived contextual instance conflicting with another, with an ambiguity error?
Two contextual values are ambiguous if both match the expected type and the
compiler is unable to find a reason why one should be chosen over the other.
There are several ways of changing the priority of given
values, but in
more complex cases, this can have the unintended consequence of causing a new
ambiguity elsewhere with a different contextual value.
The most reliable way to avoid this problem is to select the set of given
definitions that can be ambiguous, and to be explicit about their priority
using compiletime.summonFrom
.
To transform an existing set of ambiguous given
s, first change them from
given
s into ordinary def
s. For instances derived by Wisteria, this requires
the derivation to be implemented outside the companion object (see above).
Then, define the derived
given
as:
inline given derived[ValueType]: DerivationType[ValueType] =
compiletime.summonFrom:
// cases
We will specify one case for each of the previous given
definitions, in the
order that they should be attempted.
Each case should be a type pattern, a given case
or a wildcard pattern which
will use the presence of a contextual instance of the specified type (at the
callsite) to determine if that particular case should match. For example, if we
want to define derivation for a Debug
typeclass which returns the "best"
string value for a particular type, we could write it as follows:
object Debug:
inline given derived[ValueType]: Debug[ValueType] = value =>
compiletime.summonFrom:
case encoder: Encoder[ValueType] => encoder.encode(value)
case given Show[ValueType] => value.show
case _ => value.toString
In plain English, this could be interpreted as,
- if there is an
Encoder
forvalue
's type, use it to encode the value - if there is a
Show
forvalue
's type, make it available in-scope on the right-hand side of the case clause, and use it toshow
the value - otherwise, just use the
value
'stoString
method
How can I generically-derive a typeclass for a type which indirectly refers to its own type in its fields?
A recursive type such as Tree
,
enum Tree:
case Leaf
case Branch(left: Tree, value: Int, right: Tree)
cannot be derived in-place, and should be explicitly defined on that type's companion object. The easiest way to
do this is to add a derives
clause to the companion. For example,
object Tree derives Typeclass
Why does the compiler fail during derivation with a long message that mentions that, given instance derived in trait Derivation does not match type...
?
This is usually because the polymorphic lambda's type variable for delegate
or variant
is missing its upper
bound. It is essential that the type variable is specified as [VariantType <: DerivationType]
and not just,
[VariantType]
.
Why does the compiler report a type mismatch between the derivation type and Product
?
This is usually because the derivation type in the signature of join
is missing the <: Product
constraint.
Wisteria is classified as maturescent. For reference, Soundness projects are categorized into one of the following five stability levels:
- embryonic: for experimental or demonstrative purposes only, without any guarantees of longevity
- fledgling: of proven utility, seeking contributions, but liable to significant redesigns
- maturescent: major design decisions broady settled, seeking probatory adoption and refinement
- dependable: production-ready, subject to controlled ongoing maintenance and enhancement; tagged as version
1.0.0
or later - adamantine: proven, reliable and production-ready, with no further breaking changes ever anticipated
Projects at any stability level, even embryonic projects, can still be used, as long as caution is taken to avoid a mismatch between the project's stability level and the required stability and maintainability of your own project.
Wisteria is designed to be small. Its entire source code currently consists of 583 lines of code.
Wisteria will ultimately be built by Fury, when it is published. In the meantime, two possibilities are offered, however they are acknowledged to be fragile, inadequately tested, and unsuitable for anything more than experimentation. They are provided only for the necessity of providing some answer to the question, "how can I try Wisteria?".
-
Copy the sources into your own project
Read the
fury
file in the repository root to understand Wisteria's build structure, dependencies and source location; the file format should be short and quite intuitive. Copy the sources into a source directory in your own project, then repeat (recursively) for each of the dependencies.The sources are compiled against the latest nightly release of Scala 3. There should be no problem to compile the project together with all of its dependencies in a single compilation.
-
Build with Wrath
Wrath is a bootstrapping script for building Wisteria and other projects in the absence of a fully-featured build tool. It is designed to read the
fury
file in the project directory, and produce a collection of JAR files which can be added to a classpath, by compiling the project and all of its dependencies, including the Scala compiler itself.Download the latest version of
wrath
, make it executable, and add it to your path, for example by copying it to/usr/local/bin/
.Clone this repository inside an empty directory, so that the build can safely make clones of repositories it depends on as peers of
wisteria
. Runwrath -F
in the repository root. This will download and compile the latest version of Scala, as well as all of Wisteria's dependencies.If the build was successful, the compiled JAR files can be found in the
.wrath/dist
directory.
Contributors to Wisteria are welcome and encouraged. New contributors may like to look for issues marked beginner.
We suggest that all contributors read the Contributing Guide to make the process of contributing to Wisteria easier.
Please do not contact project maintainers privately with questions unless there is a good reason to keep them private. While it can be tempting to repsond to such questions, private answers cannot be shared with a wider audience, and it can result in duplication of effort.
Wisteria was designed and developed by Jon Pretty, and commercial support and training on all aspects of Scala 3 is available from Propensive OÜ.
Wisteria is a flowering plant, much like magnolia is, and Wisteria is a derivative of Magnolia.
In general, Soundness project names are always chosen with some rationale, however it is usually frivolous. Each name is chosen for more for its uniqueness and intrigue than its concision or catchiness, and there is no bias towards names with positive or "nice" meanings—since many of the libraries perform some quite unpleasant tasks.
Names should be English words, though many are obscure or archaic, and it should be noted how willingly English adopts foreign words. Names are generally of Greek or Latin origin, and have often arrived in English via a romance language.
The logo shows a hazy, floral shape in pale colors.
Wisteria is copyright © 2024 Jon Pretty & Propensive OÜ, and is made available under the Apache 2.0 License.