I am defining few ADTs representing logic formulas. They all use i.e. And constructor, but then differ in what other constructors they use. I'd like to reuse case class definitions with a hope that I could reuse some code later. I'd like to do something like:
sealed trait Formula
sealed trait PositiveFormula
case class Not(sub) extends Formula
case class And(left, right) extends Formula, PositiveFormula
But this doesn't work for any single type for sub, left and right.
So I'd like to say:
sealed trait Formula
sealed trait PositiveFormula
case class Not[A](sub : A)
Not[Formula] extends Formula
case class And(left : A, right : A)
And[Formula] extends Formula
And[PositiveFormula] extends PositiveFormula
A few questions:
Is anything like above possible and I just dont know syntax?
Is there other solution to "reuse case class constructors" problem?
What's your opinion on how useful this would be if possible?
In Scala parametric classes can't extend different parents for different type parameters (only parent's type parameter can vary).
It seems you want to have something like "Not[T] extends T", "And[T] extends T" (pseudocode). See Scala : class[T] extends T?
You can try
sealed trait Formula
sealed trait PositiveFormula
trait NotLike[A] {
def sub: A
// your generic code here
}
case class Not(sub: Formula) extends NotLike[Formula] with Formula
trait AndLike[A] {
def left: A
def right: A
// your generic code here
}
case class And(left: Formula, right: Formula) extends
AndLike[Formula] with Formula
case class PositiveAnd(left: PositiveFormula, right: PositiveFormula) extends
AndLike[PositiveFormula] with PositiveFormula
You can also introduce FormulaLike, a common parent for Formula and PositiveFormula
sealed trait FormulaLike
sealed trait Formula extends FormulaLike
sealed trait PositiveFormula extends FormulaLike
trait NotLike[A <: FormulaLike] {
def sub: A
}
trait AndLike[A <: FormulaLike] {
def left: A
def right: A
}
Is PositiveFormula a Formula or not? Just in case, if so then you can make PositiveFormula extend Formula (instead of introducing FormulaLike).
Or maybe you can try to express your relations between types with type classes (inheritance can be too restrictive, composition should be preferred over inheritance)
https://www.baeldung.com/scala/type-classes (intro to type classes)
https://kubuszok.com/2018/implicits-type-classes-and-extension-methods-part-1/ (one more intro to type classes)
https://tpolecat.github.io/2015/04/29/f-bounds.html (type classes and F-bounds)
https://github.com/milessabin/shapeless/blob/main/core/shared/src/main/scala/shapeless/ops/nat.scala (example of type-level calculations with type classes)
You can have hierarchy of classes (nodes) extending Formula and mark some of the nodes as belonging to the type class IsPositive
// hierarchy
sealed trait Formula
// type class
trait IsPositive[A <: Formula]
case class Not[A <: Formula](sub: A) extends Formula
case class And[A <: Formula](left: A, right: A) extends Formula
implicit def and[A <: Formula](implicit ev: IsPositive[A]): IsPositive[And[A]] = null
or use phantom types with type classes
// phantom types
type Formula
type PositiveFormula /*<: Formula*/
case class Not[A](sub: A)
case class And[A](left: A, right: A)
// type class
trait NotTypeclass[A] {
type Out
def apply(sub: A): Out
}
object NotTypeclass {
type Aux[A, Out0] = NotTypeclass[A] {type Out = Out0}
def instance[A, Out0](f: A => Out0): Aux[A, Out0] = new NotTypeclass[A] {
override type Out = Out0
override def apply(sub: A): Out0 = f(sub)
}
implicit def not[A <: Formula]: Aux[A, Not[A] with Formula] =
instance(sub => Not(sub).asInstanceOf[Not[A] with Formula])
}
def makeNot[A](sub: A)(implicit notTc: NotTypeclass[A]): notTc.Out = notTc(sub)
// type class
trait AndTypeclass[A] {
type Out
def apply(left: A, right: A): Out
}
trait LowPriorityAnd {
type Aux[A, Out0] = AndTypeclass[A] {type Out = Out0}
def instance[A, Out0](f: (A, A) => Out0): Aux[A, Out0] = new AndTypeclass[A] {
override type Out = Out0
override def apply(left: A, right: A): Out0 = f(left, right)
}
implicit def and[A <: Formula]: Aux[A, And[A] with Formula] =
instance((l, r) => And(l, r).asInstanceOf[And[A] with Formula])
}
object AndTypeclass extends LowPriorityAnd {
implicit def positiveAnd[A <: PositiveFormula]: Aux[A, And[A] with PositiveFormula] =
instance((l, r) => And(l, r).asInstanceOf[And[A] with PositiveFormula])
}
def makeAnd[A](left: A, right: A)(implicit andTc: AndTypeclass[A]): andTc.Out =
andTc(left, right)
makeAnd(??? : PositiveFormula, ???): PositiveFormula
makeAnd(??? : Formula, ???): Formula
makeNot(??? : Formula): Formula
// makeNot(??? : PositiveFormula) // doesn't compile
makeNot(makeAnd(??? : Formula, ???)): Formula
makeAnd(makeNot(??? : Formula), makeAnd(??? : Formula, ???)): Formula
makeAnd(makeAnd(??? : Formula, ???), makeAnd(??? : Formula, ???)): Formula
makeAnd(makeAnd(??? : PositiveFormula, ???), makeAnd(??? : PositiveFormula, ???)): PositiveFormula
or use phantom types keeping type classes only for type-level calculations
type Formula
type PositiveFormula /*<: Formula*/
case class Not[A](sub: A)
case class And[A](left: A, right: A)
trait NotTypeclass[A] {
type Out
}
object NotTypeclass {
type Aux[A, Out0] = NotTypeclass[A] { type Out = Out0 }
implicit def not[A <: Formula]: Aux[A, Formula] = null
}
def makeNot[A](sub: A)(implicit
notTc: NotTypeclass[A]
): Not[A] with notTc.Out = Not(sub).asInstanceOf[Not[A] with notTc.Out]
trait AndTypeclass[A] {
type Out
}
trait LowPriorityAnd {
type Aux[A, Out0] = AndTypeclass[A] {type Out = Out0}
implicit def and[A <: Formula]: Aux[A, Formula] = null
}
object AndTypeclass extends LowPriorityAnd {
implicit def positiveAnd[A <: PositiveFormula]: Aux[A, PositiveFormula] = null
}
def makeAnd[A](left: A, right: A)(implicit
andTc: AndTypeclass[A]
): And[A] with andTc.Out = And(left, right).asInstanceOf[And[A] with andTc.Out]
or use phantom types without type classes
type Formula
type PositiveFormula /*<: Formula*/
case class Not[A](sub: A)
case class And[A](left: A, right: A)
def makeNot[A <: Formula](sub: A): Not[A] with Formula =
Not(sub).asInstanceOf[Not[A] with Formula]
def makeAnd[A <: Formula](left: A, right: A): And[A] with Formula =
And(left, right).asInstanceOf[And[A] with Formula]
def makePositiveAnd[A <: PositiveFormula](left: A, right: A): And[A] with PositiveFormula =
And(left, right).asInstanceOf[And[A] with PositiveFormula]
Formula and PositiveFormula are abstract types rather than traits to avoid ClassCastException.
You need types for case class fields. You can probably make PositiveFormula extend Formula too. For example, this would work for Boolean operands.
sealed trait Formula
sealed trait PositiveFormula extends Formula
case class Not(sub:Boolean) extends Formula
case class And(left:Boolean, right:Boolean) extends PositiveFormula
Related
The problem I'm looking at involves defining a trait which has type parameters, where it's type parameters have type parameters where I need to know these types as well. For example, consider the following code:
trait JoinData[A <: Attributes,
L <: Entity[A],
B <: Keyed
R <: Document[B],
O <: Output[A, B]] extends Something[A,L,B,R]{
def method1(a:A) : String
def method2(L, R) : O = {
...
}
...
}
When the user implements this, the types L and R do dictate what A and B have to be, however I'm not sure if it's possible to remove A and B from my API.
This problem is resulting in pretty ugly APIs, where it's pretty difficult for a user to understand why they are having to specify some of the types, or even what they should be.
Can anyone point me at some ideas of how I might resolve this (if at all possible!).
It's hard to answer your question without any details about your API and how you use it. Currently it's not even clear why monomorphic
trait JoinData extends Something {
def method1(a: Attributes): String
def method2(e: Entity, r: Document): Output = {
...
}
...
}
is insufficient.
You can try to replace (some of) type parameters with type members:
trait Attributes
trait Entity {
type A <: Attributes
}
trait Keyed
trait Document {
type B <: Keyed
}
trait Something[L <: Entity, R <: Document]
trait Output[A <: Attributes, B <: Keyed]
trait JoinData[L <: Entity, R <: Document] extends Something[L, R]{
def method1(a: L#A): String
def method2(l: L, r: R): Output[L#A, R#B] = ???
}
using type projections or
trait Attributes
trait Entity
trait Keyed
trait Document
trait Something[L <: Entity, R <: Document]
trait Output[A <: Attributes, B <: Keyed]
trait TC[L <: Entity] {
type A <: Attributes
}
trait TC1[R <: Document] {
type B <: Keyed
}
abstract class JoinData[L <: Entity, R <: Document](implicit val tc: TC[L]) extends Something[L, R]{
def method1(a: tc.A) : String
def method2(l: L, r: R)(implicit tc1: TC1[R]): Output[tc.A, tc1.B] = ???
}
using type classes.
I have a trait with covariant type A that declares (among others) this method:
sealed trait MaxPQ[+U] {
...
def insert[K >: U : Ordering](v: K): this.type
...
}
abstract class AbstractMaxPQ[U : Ordering] extends MaxPQ[U]
I also have this mix-in that I need to use:
trait FindMin[U] {
self: AbstractMaxPQ[U] =>
I am returning this.type because I want Node or Array based implementations of the priority queue to return current types and not MaxPQ[U].
In the array based implementation I have the following:
class ArrayMaxPQ[U : Ordering : ClassTag] ...extends AbstractMaxPQ[U] with FindMin[U]{
...
override def insert[K >: U : Ordering](v: K): ArrayMaxPQ[U] = ???
...
this is what is auto-generated by my IDE. Of course I need this method to return ArrayMaxPQ[K].
this.type doesn't take type parameters. Using higher-kinded types here also wouldn't work
def insert[K >: U : Ordering, G[_] <: MaxPQ[_]](v: K): G[K]
since G cannot be parameterized with self type in children.
I am a bit confused since this feels like a simple requirement but I cannot find language feature/syntax required to implement it.
Maybe you want the return type to be wider than this.type but narrower than just MaxPQ[U]. Then try to introduce a type member
sealed trait MaxPQ[+U] {
type This <: MaxPQ[U]
// type This >: this.type <: MaxPQ[U] { type This = MaxPQ.this.This }
def insert[K >: U : Ordering](v: K): This
}
abstract class AbstractMaxPQ[U : Ordering] extends MaxPQ[U] {
override type This <: AbstractMaxPQ[U]
// override type This >: this.type <: AbstractMaxPQ[U] { type This = AbstractMaxPQ.this.This }
}
class ArrayMaxPQ[U : Ordering : ClassTag] extends AbstractMaxPQ[U] {
override type This = ArrayMaxPQ[U]
override def insert[K >: U : Ordering](v: K): ArrayMaxPQ[U] = ???
}
Returning the "Current" Type in Scala
If you want the return type to be parametrized with an upper bound of U try to make This higher-kinded
sealed trait MaxPQ[+U] {
type This[K >: U] <: MaxPQ[K]
// type This[K >: U] <: MaxPQ[K] { type This[K1 >: K] = MaxPQ.this.This[K1] }
def insert[K >: U : Ordering](v: K): This[K]
}
abstract class AbstractMaxPQ[U : Ordering] extends MaxPQ[U] {
override type This[K >: U] <: AbstractMaxPQ[K]
// override type This[K >: U] <: AbstractMaxPQ[K] { type This[K1 >: K] = AbstractMaxPQ.this.This[K1] }
}
class ArrayMaxPQ[U : Ordering : ClassTag] extends AbstractMaxPQ[U] {
override type This[K >: U] = ArrayMaxPQ[K]
override def insert[K >: U : Ordering](v: K): ArrayMaxPQ[K] = ???
}
You can actually use higher-kinded types to do this. It's called F-bounded polymorphism.
sealed trait MaxPQ[+U, F[_]] { self: F[_ <: U] =>
def insert[K >: U: Ordering](v: K): F[K]
}
abstract class AbstractMaxPQ[U: Ordering, F[_]] extends MaxPQ[U, F] {
self: F[U] =>
}
class ArrayMaxPQ[U: Ordering: ClassTag] extends AbstractMaxPQ[U, ArrayMaxPQ] {
override def insert[K >: U: Ordering](v: K): ArrayMaxPQ[K] = ???
}
Scastie demo
Instead of passing G into insert, use it as a type parameter of the class/trait itself, and then make sure that this extends that. That's what the self-type is for (self: F[U]). Another useful question
[ I was unable to explain the problem with less verbosity. The core of the issue is that the compiler infers an underscore (_) type. In particular, [_ >: SomeType <: SomeOtherType]. I am clueless about when, how and why that is possible ]
As a scala exercise, I am trying to encode a vector of elements with a given size.
As a necessary step, I started by copying an existing encoding of natural numbers:
sealed trait Natural extends Product with Serializable {
def +(that: Natural): Natural
}
case object Zero extends Natural {
override def +(that: Natural): Natural = that
}
final case class Suc[N <: Natural](n: N) extends Natural {
override def +(that: Natural): Natural = Suc(n + that)
}
I believe the following diagram is a faithful portrayal of the type relation:
I then attempted to model a vector parameterized by a type on the elements and another type on size. To explain the problem though, I assumed a vector of ints and parameterized only the size of the vector :
import shapeless.=:!=
sealed trait Vector[+Size <: Natural] extends Product with Serializable {
def head: Int
def tail: Vector[Natural]
def plus[OtherSize >: Size <: Natural]
(that: Vector[OtherSize])
(implicit ev: OtherSize =:!= Natural): Vector[OtherSize]
}
case object VectorNil extends Vector[Zero.type] {
override def head: Nothing = throw new Exception("Boom!")
override def tail: Vector[Zero.type] = throw new Exception("Boom")
override def plus[OtherSize >: Zero.type <: Natural]
(that: Vector[OtherSize])
(implicit ev: =:!=[OtherSize, Natural]): Vector[OtherSize] = this
}
final case class VectorCons[N <: Natural](
head: Int,
tail: Vector[N]
) extends Vector[Suc[N]] {
override def plus[OtherSize >: Suc[N] <: Natural]
(that: Vector[OtherSize])
(implicit ev: =:!=[OtherSize, Natural]): Vector[OtherSize] = that
}
Notice that VectorCons[N] is actually a Vector of size Suc[N]. (extends Vector[Suc[N]]).
Method plus should add the elements of two vectors of the same size. I wanted to raise to the type level the verification that you can only sum vectors of the same size.
Notice that the conjunction of the type bounds OtherSize >: Size <: Natural with the implicit evidence should achieve that (see similar example at the bottom), but:
val foo = VectorCons(1, VectorCons(2, VectorNil))
//type -> VectorCons[Suc[Zero.type]]
// note that foo is (can be viewed) as Vector[Suc[Suc[Zero.type]], i.e
// a vector of 2 elements
val bar = VectorCons(3, VectorNil)
//type -> VectorCons[Zero.type]
val baz = foo.plus(bar)
//type -> Vector[Suc[_ >: Suc[Zero.type] with Zero.type <: Natural]] !! How is this possible ?? !!
to my frustration, baz compiles just fine!! The shapeless type constrain doesn't work; well, because OtherSize really is different from Natural; particularly, it is Suc[_ >: Suc[Zero.type] with Zero.type <: Natural].
So, I am very much confused with the type of baz! This is what allows the constraint to be bypassed.
What did the compiler infer for the type of baz/bar?
Is that an existential type ?
Is the compiler allowed to infer such things?
Is that not undesirable behavior ?
At this point, I am not concerned whether or not this is the correct encoding for a vector. Just wanted to understand how can the compiler infer that type for baz ?
p.s
1 - I am aware the return that on the implementation of method plus on VectorCons does not achieve what the plus semantics implies, but that is not important for the question.
###### Extra #######
I suspect this weird (at least for me) behavior has something to to with the natural numbers. The following code works fine!! :
[Disregard the absurd semantics of the code]
sealed trait Animal extends Product with Serializable
case class Dog() extends Animal
case class Cow() extends Animal
case object NoBox extends Animal //Not important
and,
trait Vector[+A <: Animal] {
def head: A
def plus[That >: A <: Animal]
(that: Vector[That])
(implicit ev: =:!=[That, Animal]): Vector[That]
}
case object Nil extends Vector[NoBox.type] {
def head: Nothing = throw new NoSuchElementException("Boom!")
override def plus[That >: NoBox.type <: Animal]
(that: Vector[That])(implicit ev: =:!=[That, Animal]): Vector[That] = this
}
case class Cons[A <: Animal](head: A) extends Vector[A] {
override def plus[That >: A <: Animal]
(that: Vector[That])(implicit ev: =:!=[That, Animal]): Vector[That] = that
}
whereby:
val foo = Cons(Dog())
val bar = Cons(Cow())
// Compiles
val baz = foo.plus(foo)
val baz2 = bar.plus(bar)
// Does not compile (what I would expect)
val baz3 = bar.plus(foo)
val baz4 = foo.plus(bar)
Thank you for your input,
I am designing a F-Bound data type, and have a working companion object. I would like to reference this companion object from the trait itself, but I can't get the types right.
trait A[AA <: A[AA]] {
self =>
val data: String
}
case class A1(data : String) extends A[A1]
trait B[BB <: B[BB, AA], AA <: A[AA]] {
self: BB =>
val content: AA
def companion: BComp[BB, AA] // What is the correct type?
def companion2: BComp2[BB, AA] // What is the correct type?
}
trait BComp[BB[X <: BB[X, AA], Y <: AA[Y]], AA[Y <: AA[Y]]]
trait BComp2[BB[X <: AA[X]], AA[X <: AA[X]]]
case class BInst[AA <: A[AA]](content: AA) extends B[BInst[AA], AA] {
def companion = BInst
def companion2 = BInst2
}
object BInst extends BComp[B, A]
object BInst2 extends BComp2[BInst, A]
A working solution for either companion or companion2 would suffice, although a general hint on how to construct these type signatures would be useful.
edit
I want to use the companion object to store canBuildFrom style implicits as well as Builders, but as the content type A has an upper bound, all generating functions need to be aware of this bounding, so hence the parametrization of the companion object trait. The inspiration for this design comes from GenericCompanion.scala, of course, adding the type bounds makes everything more difficult :P
The obstacle to any clear definition of types for companion is that BComp is parametrized by two parametrized types - while the first one is compatible with B, the second one AA[Y <: AA[Y]] is impossible to construct. So quite simply, we need to add this type to the parametrization of B:
trait B[BB <: B[BB, AA, X],
AA[T <: AA[T]] <: A[T],
X <: AA[X]] {
self: BB =>
val content: X
def companion: BComp[B, AA]
}
now we have a compatible type AA for our companion object (which needs only a small expansion):
trait BComp[BHere[BB <: BHere[BB, AAA, Z],
AAA[Y <: AAA[Y]],
Z <: AAA[Z]],
AA[Y <: AA[Y]]]
Isn't that pretty?
case class BInst[X <: A[X]](content: X) extends B[BInst[X], A, X] {
def companion: BComp[B, A] = BInst5
}
object BInst extends BComp[B, A]
companion2
For companion2, we just need to change the first part of the parametrization of B, so that trait B and the companion trait become:
trait B[BB[TS <: AA[TS]] <: B[BB, AA, TS],
AA[T <: AA[T]] <: A[T],
X <: AA[X]] {
self: BB[X] =>
val content: X
def companion2: BComp[BB, AA]
}
trait BComp[BHere[TS <: AA[TS]],
AA[Y <: AA[Y]]]
This is slightly more manageable. The case class and case objects are:
case class BInst[X <: A[X]](content: X) extends B[BInst, A, X] {
def companion2: BComp[BInst, A] = BInst5
}
object BInst extends BComp[BInst, A]
If this is useful to anybody the rethink. I have changed my own design approach and I suggest you do too. Coming up with a solution to these types has now been a purely academic exercise!
I have a F-Bound type:
sealed trait A[AA <: A[AA]] {
self: AA =>
}
And a second F-Bound type, that is parametrized by the first type.
sealed trait B[BB <: B[BB, AA], AA <: A[AA]] {
self: BB =>
val content: AA
}
I can happily write case classes that makes use of these types:
case class BInst[BB <: BInst[BB, AA], AA <: A[AA]](content: AA)
extends B[BInst[BB, AA], AA]
Now I would like to have a companion object for the case class, which I can reference through the trait B, something like:
sealed trait A[AA <: A[AA]] { self: AA => }
sealed trait B[BB <: B[BB, AA], AA <: A[AA]] {
self: BB =>
val content: AA
def companion: Companion[BB]
}
case class BInst[BB <: BInst[BB, AA], AA <: A[AA]](content: AA)
extends B[BInst[BB, AA], AA] {
def companion: Companion[BInst[BB, AA]] = BInst
}
sealed trait Companion[+BB <: B[_, _]]
object BInst extends Companion[BInst]
But this fails to compile, as BInst in the companion parametrization (last line) requires type parameters.
Similarly
sealed trait Companion[BB[X, Y] <: B[X, Y]]
fails. What is the correct type for the companion object?
The only way I see is to abandon generic parameters for Companion/BInst as there might be only one instance of object even from type perspective (only one BInst.type type):
scala> sealed trait A[AA <: A[AA]] { self: AA => }
sealed trait B[BB <: B[BB, AA], AA <: A[AA]] {
self: BB =>
val content: AA
def companion: Companion[_]
}
case class BInst[BB <: BInst[BB, AA], AA <: A[AA]](content: AA)
extends B[BInst[BB, AA], AA] {
def companion = BInst
}
sealed trait Companion[+BB <: B[_, _]]
object BInst extends Companion[BInst[_, _]]
defined trait A
defined trait B
defined class BInst
defined trait Companion
defined module BInst
After that you may actually cast your BInst (inside case class):
def companion: Companion[BInst[BB, AA]] =
BInst.asInstanceOf[Companion[BInst[BB, AA]]
If you don't cast types inside your Companion-trait/BInst-object (it's better to use only BB/AA-independent methods of BInst class) there is no risk to get ClassCastException, .asInstanceOf[Companion[BInst[BB, AA]]] will just create (clone) new type for you, using BInst as prototype.
I've tried to merge what you have written here with your other question:
scala: Referencing Trait with F-Bound Parameters
This is as close as I could get to your code by keeping it compilable:
import scala.language.higherKinds
trait A[AA <: A[AA]] { self: AA => }
trait B[
X <: A[X],
This[Y <: A[Y]] <: B[Y, This]
] {
self: This[X] =>
def content: X
def companion: Companion[This]
}
trait Companion[Coll[X <: A[X]] <: B[X, Coll]]
case class BInst[X <: A[X]](content: X)
extends B[X, BInst] {
def companion: Companion[BInst] = BInst
}
object BInst extends Companion[BInst]
Your BBs have been replaced by This, order of arguments has been swapped so that it is more similar to what is done in the standard collection library.
A general remark.
You don't seem to be quite sure about what you want from your BInst and BComp/Companion. It the two versions of your questions, they seem to have completely different types. If you are not quite sure what you want, it might be advisable to just keep it as simple as possible. There is anecdotal evidence that all these F-Bound-GenericCompanionCanBuildFromFactories can be difficult to get right.