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!
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.
Let's set up issue conditions
trait Bound
trait Bound2 extends Bound
trait T1[B <: Bound]
trait T2[B <: Bound2] extends T1[B]
trait WrapperT1[Tz[B2 <: Bound] <: T1[B2]]
This code compile without problem, issue comes when trying to extend WrapperT1
trait WrapperT2[Tz[B2 <: Bound2] <: T2[B2]] extends WrapperT1[T2]
// Compilator error
kinds of the type arguments (T2) do not conform to the expected kinds of the type parameters (type Tz) in trait WrapperT1.
[error] ex.T2's type parameters do not match type Tz's expected parameters:
[error] type B's bounds <: Bound2 are stricter than type B2's declared bounds <: ex.Bound
[error] trait WrapperT2[Tz[B2 <: Bound2] <: T2[B2]] extends WrapperT1[T2]
Yes B2 <: Bound2 is stricter than B2 <: Bound but i'm not understanding why the compilator complains for this reason and i would be grateful to know more about it.
Potential solution but does it has some drawbacks ?
trait Bound
trait Bound2 extends Bound
trait T1[B] {
implicit val ev: B <:< Bound
}
trait T2[B] extends T1[B] {
// this is possible thank's to covariance of class `<:<[-From, +To]` if i'm not wrong
implicit val ev: B <:< Bound2
}
trait WrapperT1[Tz[B2] <: T1[B2]]
// Compiles
trait WrapperT2[Tz[B2] <: T2[B2]] extends WrapperT1[Tz]
This looks nice and we keep the compilation checks about B2 generic type but is there any inconveniant using it ?
To understand why the compiler complains, let's look at this piece of code (which is the same as your code, but with different names)
trait Show[A <: AnyRef] {
def show(a: A): Unit
}
trait ShowString[S <: CharSequence] extends Show[S] {
def show(s: S): Unit = println(s)
}
trait Wrapper1[S[A <: AnyRef] <: Show[A]] {
def show[A <: AnyRef](a: A)(implicit show: S[A]): Unit = show.show(a)
}
trait Wrapper2[S[C <: CharSequence] <: ShowString[C]] extends Wrapper1[S]
Here, too, the compiler emits the error type C's bounds <: CharSequence are stricter than type A's declared bounds <: AnyRef, but let's pretend you somehow get it to compile here. That would mean if someone wanted to use your wrapper class to print, say, a list, they wouldn't be able to do that, meaning that your wrapper class would be basically useless.
val w2: Wrapper[ShowString] = ???
w2.show(List(1, 2))
//Error: could not find implicit value for parameter show: ShowString[List[Int]]
In this case, it's just that the implicit isn't found. However, if you have a method in wrapper that accepts a S[_] (or Tz[_], from your example) and you try to shoehorn an object of the wrong type into your show method, you could get a runtime exception.
It fails for the same reason you can't do this
trait T1[A] {
def process(a: A): Unit
}
trait T2 extends T1[Int] {
def process(i: Int): Unit = ???
}
val t2: T2 = ???
t2.process("not an int")
T2 is just not equipped to handle anything that is not an Int, and the same applies here.
I have a parallel hierarchy of types that generically reference each other: One through plain generics, the other via a type variable.
However, one of the parallel hierarchies is shorter than the other, and this is causing me issues.
I have provided a code snippet below, and the compiler errors are added as inline comments above the lines that cause the issues:
type arguments [Foo[_],FooKey] do not conform to trait PrimaryKey's type parameter bounds [A <: Entity[A],B <: PrimaryKey[A,B]]
overriding type K in trait Entity with bounds <: PrimaryKey[A,Foo.this.K]; type K has incompatible type
The issue is that I want FooKey to not be generic, as for FooKey it doesn't matter how exactly the implementation of Foo is parameterized.
package com.scalatest
object Database {
trait PrimaryKey[A <: Entity[A], B <: PrimaryKey[A, B]] {
this: B =>
}
trait Entity[A <: Entity[A]] {
this: A =>
type K <: PrimaryKey[A, K]
val id: K
}
//Error:(17, 24) type arguments [com.scalatest.Database.Foo[_],com.scalatest.Database.FooKey] do not conform to trait PrimaryKey's type parameter bounds [A <: com.scalatest.Database.Entity[A],B <: com.scalatest.Database.PrimaryKey[A,B]]
// class FooKey extends PrimaryKey[Foo[_], FooKey]
class FooKey extends PrimaryKey[Foo[_], FooKey]
trait Foo[A <: Foo[A]] extends Entity[A] {
this: A =>
//Error:(24, 19) overriding type K in trait Entity with bounds <: com.scalatest.Database.PrimaryKey[A,Foo.this.K];
// type K has incompatible type
// override type K = FooKey
override type K = FooKey
}
class FooImpl(val id: FooKey) extends Foo[FooImpl]
}
I managed to use an existential type instead of Foo[_] in my declaration of FooKey, in conjunction with making all As covariant, but then I am still left with the 2nd error.
class FooKey extends PrimaryKey[Foo[A] forSome { type A <: Foo[A] }, FooKey]
It's not true that
for FooKey it doesn't matter how exactly the implementation of Foo is parameterized.
You want to override with FooKey the type K <: PrimaryKey[A, K] where A is the same as in trait Entity[A <: Entity[A]] {...
And you make this overriding in trait Foo[A <: Foo[A]] {... so FooKey must satisfy condition K <: PrimaryKey[A, K] (this is not just upper bound since K is present in both sides) for every A <: Foo[A] so FooKey should be quantified universally rather than existentially.
So you should add type parameter to FooKey
object Database {
trait PrimaryKey[A <: Entity[A], B <: PrimaryKey[A, B]] {
this: B =>
}
trait Entity[A <: Entity[A]] {
this: A =>
type K <: PrimaryKey[A, K]
val id: K
}
class FooKey[A <: Foo[A]] extends PrimaryKey[A, FooKey[A]]
trait Foo[A <: Foo[A]] extends Entity[A] {
this: A =>
override type K = FooKey[A]
}
class FooImpl(val id: FooKey[FooImpl]) extends Foo[FooImpl]
}
I have kind of a complex type hierarchy, but to break it down there are two base traits: Convertable and Conversion[A <: Convertable, B <: Convertable, e.g. there is a Conversion which can convert a Mealy-automaton to a Moore-automaton.
Every Conversion[A,B] has a convert(automaton: A) : B method.
Now I want to introduce the concept of smart Conversions, which are basically a List of normal Conversions, which will be performed one after another.
Therefore I have introduced an AutoConversion trait, extending a Conversion, which has a val path : HList parameter, to represent the chain of conversions, and should implement the convert method, so that AutoConversions just have to provide the list of actual Conversions to take.
I think you could implement this with a fold over the path, so here is my first try:
package de.uni_luebeck.isp.conversions
import shapeless._
import shapeless.ops.hlist.LeftFolder
trait AutoConversion[A <: Convertable, B <: Convertable] extends Conversion[A, B] {
val path: HList
object combiner extends Poly {
implicit def doSmth[C <: Convertable, D <: Convertable] =
use((conv : Conversion[C, D] , automaton : C) => conv.convert(automaton))
}
override def convert(startAutomaton: A): B = {
path.foldLeft(startAutomaton)(combiner)
}
}
This won't work, because no implicit Folder can be found, so I'm guessing I have to provide more Type information for the Compiler somewhere, but don't know where
You're right about needing more type information, and in general if you have a value with HList as a static type, it's likely you'll need to change your approach. There's essentially nothing you can do with an HList if all you know is that it's an HList (besides prepend values to it), and you'll usually only ever write HList as a type constraint.
In your case what you're describing is a kind of type-aligned sequence. Before you move forward with this approach, I'd suggest being really sure that you actually need to. One of the nice things about functions (and function-like types like your Conversion) is that they compose: you have an A => B and a B => C and you compose them into an A => C and can forget about B forever. You get a nice clean black box, which is generally exactly what you want.
In some cases, though, it can be useful to be able to compose function-like things in such a way that you can reflect on the pieces of the pipeline. I'm going to assume that this is one of those cases, but you should confirm that for yourself. If it's not, you're in luck, because what's coming is kind of messy.
I'll assume these types:
trait Convertable
trait Conversion[A <: Convertable, B <: Convertable] {
def convert(a: A): B
}
We can define a type class that witnesses that a specific HList is composed of one or more conversions whose types line up:
import shapeless._
trait TypeAligned[L <: HList] extends DepFn1[L] {
type I <: Convertable
type O <: Convertable
type Out = Conversion[I, O]
}
L contains all of the type information about the pipeline, and I and O are the types of its endpoints.
Next we need instances for this type class (note that this must be defined together with the trait above for the two to be companioned):
object TypeAligned {
type Aux[L <: HList, A <: Convertable, B <: Convertable] = TypeAligned[L] {
type I = A
type O = B
}
implicit def firstTypeAligned[
A <: Convertable,
B <: Convertable
]: TypeAligned.Aux[Conversion[A, B] :: HNil, A, B] =
new TypeAligned[Conversion[A, B] :: HNil] {
type I = A
type O = B
def apply(l: Conversion[A, B] :: HNil): Conversion[A, B] = l.head
}
implicit def composedTypeAligned[
A <: Convertable,
B <: Convertable,
C <: Convertable,
T <: HList
](implicit
tta: TypeAligned.Aux[T, B, C]
): TypeAligned.Aux[Conversion[A, B] :: T, A, C] =
new TypeAligned[Conversion[A, B] :: T] {
type I = A
type O = C
def apply(l: Conversion[A, B] :: T): Conversion[A, C] =
new Conversion[A, C] {
def convert(a: A): C = tta(l.tail).convert(l.head.convert(a))
}
}
}
And now you can write a version of your AutoConversion that keeps track of all of the type information about the pipeline:
class AutoConversion[L <: HList, A <: Convertable, B <: Convertable](
path: L
)(implicit ta: TypeAligned.Aux[L, A, B]) extends Conversion[A, B] {
def convert(a: A): B = ta(path).convert(a)
}
And you can use it like this:
case class AutoA(i: Int) extends Convertable
case class AutoB(s: String) extends Convertable
case class AutoC(c: Char) extends Convertable
val ab: Conversion[AutoA, AutoB] = new Conversion[AutoA, AutoB] {
def convert(a: AutoA): AutoB = AutoB(a.i.toString)
}
val bc: Conversion[AutoB, AutoC] = new Conversion[AutoB, AutoC] {
def convert(b: AutoB): AutoC = AutoC(b.s.lift(3).getOrElse('-'))
}
val conv = new AutoConversion(ab :: bc :: HNil)
And conv will have the expected static type (and implement Conversion[AutoA, AutoC]).
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.