Improving ugly parameter list on traits - scala

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.

Related

Type bounds constraints restrictions in Higher Kinds when extending a trait

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.

General way of ensuring implicit definition always has higher/lower priority

I have a somewhat complex typeclass situation in the following format:
sealed trait TypeClass[S <: MyType] {
type Out <: MyType
}
sealed trait LowPriorityTypeClass {
// Case: OtherTypeClass is NOT defined for the input type S.
// The output type is the same as the input type.
implicit def default[S <: MyType]: TypeClass.Aux[S, S] = ???
}
object TypeClass extends LowPriorityTypeClass {
type Aux[S <: MyType, O <: MyType] = TypeClass[S] { type Out = O }
// Case: OtherTypeClass is defined for the input type S.
// The output type is the same as in the OtherTypeClass definition.
implicit def hasOtherTC[S <: MyType, O <: MyType](
implicit otherTC: OtherTypeClass.Aux[S, O],
): TypeClass.Aux[S, O] = ???
}
The default definition was put in the LowPriorityTypeClass trait with the intention of having a lower priority. However, an ambiguity with hasOtherTC still happens for some type S, apparently because the declaration of default is more specific than the declaration of hasOtherTC for that type S.
Is there a general way to ensure that an implicit definition will always have a higher/lower priority than other definition? (My question is not for the specific code above.)
Let me know if posting a more complete sample code would help.
Please see Why is this implicit ambiguity behaviour happening? including comments.
There is no sense in introducing trait LowPriorityTypeClass in this case because anyway implicit default is more specific than hasOtherTC.
There is no general way. You can use type classes Not (shapeless.Refute, implicitbox.Not) or shapeless.LowPriority, implicitbox.Priority or library https://github.com/milessabin/export-hook.
object TypeClass {
type Aux[S <: MyType, O <: MyType] = TypeClass[S] {type Out = O}
implicit def hasOtherTC[S <: MyType, O <: MyType](implicit
otherTC: OtherTypeClass.Aux[S, O]
): TypeClass.Aux[S, O] = ???
implicit def default[S <: MyType](implicit
noOtherTC: Refute[OtherTypeClass[S]]
): TypeClass.Aux[S, S] = ???
}

Inference of underscore types

[ 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,

Fold over HList with unknown Types

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]).

scala: Referencing Trait with F-Bound Parameters

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!