type F[-A] = [B <: A] => B => Either[String, B]
class G[-A]() {
def consumesFOrG[B <: A](fOrG: F[B] | G[B]) = ???
}
case class Data(n: Int)
def mustReturnF[A](f: A => Boolean): F[A] = ???
G[Data]().consumesFOrG(mustReturnF(_.n == 1))
results in failure with the error message of
[error] | G[Data]().consumesFOrG(mustReturnF(_.n == 1))
[error] | ^^^
[error] | value n is not a member of Any
Could you guys help me to figure out why the compiler cannot infer mustReturnF type parameter to be Data and to find workarounds to make it compilable without changing the last line, mustReturnF's return type or overloading consumesFOrG?
Try using mustReturnF[Data](_.n == 1). I don’t think it’s the union type that’s being widened to Any, it’s the missing type parameter of mustReturnF (which makes sense).
âžś scala
Welcome to Scala 3.1.3 (11.0.16, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
scala> type F[-A] = [B <: A] => B => Either[String, B]
// defined alias type F[-A] = [B <: A] => (B) => Either[String, B]
scala> class G[-A]:
| def consumesFOrG[B <: A](fOrG: F[B] | G[B]) = ???
|
// defined class G
scala> case class Data(n: Int)
// defined case class Data
scala> def mustReturnF[A](f: A => Boolean): F[A] = ???
def mustReturnF[A](f: A => Boolean): F[A]
scala> G[Data].consumesFOrG(mustReturnF[Data](_.n == 1))
scala.NotImplementedError: an implementation is missing
at scala.Predef$.$qmark$qmark$qmark(Predef.scala:344)
at rs$line$4$.mustReturnF(rs$line$4:1)
... 34 elided
scala>
Thank you for the input but I would like to avoid explicitly declare mustReturnF type parameter since it seems that the context has enough information to infer that.
In my mind, given G[Data], the method consumesFOrG argument is upper-bounded by (F[Data] | G[Data]), hence mustReturnF return type is upper-bounded by it as well, that is, F[A] <: (F[Data] | G[Data]), which implies that A <: Data. Thus inferring that A = Any looks unintuitive to me.
Regarding the type widening: I’ve meant the union type (looked as B => F[B] | G[B]) constructor’s argument (B). I will update the title to reflect that better.
I keep seeing this kind of desire in people. No offense to you but why is everyone so obsessed with not providing type parameters? Just provide them and move on! What’s the big deal? Why define things with type parameters if you don’t want to use them? It’s only natural to expect that we have to provide type parameters, if we define them that way. To me it seems like people want crazy mind reading abilities from the compiler…
Anyway, sorry if I sound angry here…
I strongly recommend reading Chapter 18: Type Parametrization of Programming in Scala 5th edition which goes over type inference with variance in detail and shows its limitations. Generally speaking, it cannot magically carry a type parameter from the left all the way to the right and inside parentheses (even in situations where we humans can cleverly prove that it can be carried). To resolve the limitations, the book says: “provide type parameters explicitly.”
There are two type parameters missing, one for consumesFOrG and one for mustReturnF. In fact even this doesn’t work:
G[Data].consumesFOrG[Data](mustReturnF(_.n == 1))
-- [E008] Not Found Error: -------------------------------------------------------------------------------------------
1 |G[Data].consumesFOrG[Data](mustReturnF(_.n == 1))
| ^^^
| value n is not a member of Any
1 error found
I really don’t think it does.
Sorry, it doesn’t (even without contravariance). Even with covariance it still doesn’t imply that. If F[_] was covariant, then A <: Data would imply F[A] <: F[Data] but not the other way around.
Even if we make it invariant, it still doesn’t work. We cannot make it covariant due to the fact that you want A to be an upper bound:
scala> type F[+A] = [B <: A] => B => Either[String, B]
-- Error: ------------------------------------------------------------------------------------------------------------
1 |type F[+A] = [B <: A] => B => Either[String, B]
| ^^
| covariant type parameter A occurs in contravariant position in [B <: A] => (B) => Either[String, B]
1 error found
The motivation is the same: inferring types which are “too precise” can lead to unintuitive typechecking issues later on. Note: Since this behavior limits the usability of union types, it might be changed in the future. For example by not widening unions that have been explicitly written down by the user and not inferred, or by not widening a type argument when the corresponding type parameter is covariant. See PR #2330 and Issue #4867 for further discussions.
This is why I avoid going too deeply into relying on Scala’s type inference, because I can’t make sense or keep track of all these different cases and design decisions. Basically Scala’s types don’t work how we “naively expect them” to work. I understand that they have their reasons for the design decisions, but I can’t make sense of them. It’s too much for me.
I just provide the type parameter, always. I changed my mindset to think of type parameters like the same as normal value parameters.
The relevant part in the book is Chapter 17.6 Union types.
There is an interesting point that might be relevant:
You can invoke any method or access any field defined on any of the constituent types of an intersection type. On an instance of Plum & Apricot, for example, you can invoke any methods defined in either Plum or Apricot. By contrast, on a union type, you can only access members of supertypes that are common to the constituent types. On an instance of Plum | Apricot, therefore, you can access the members of Fruit (including members it inherits from AnyRef and Any), but you cannot access any members added in either Plum or Apricot.
This might explain why we see Any when we try to use the method _.n.
I know! I don’t care much for inheritance/OOP either. Type class derivation is much nicer. Anyway, I was just giving off subjective opinion, didn’t mean to start a discussion about that… I’ll remove that part of my comment. I should have stayed on topic, my bad.
Hey, @spamegg1, thank you for putting all this together!
I’ve been reading the reference documentation, chapters 17.6 and 18 of Programming in Scala, doing some experiments, and come to a conclusion not very far from yours regarding type union inference workings: it really seems necessary to explicitly declare the type parameter one way or another.
Now regarding the instantiation of a value of type F[A], note that it is a polymorphic function while what you have tried to assign to it is a monomorphic curried function of type A => A => Either[String, A]. The difference being that the first argument should be a type (B) upper-bounded A and not a instance of type A. So the syntax for declaring a value of type F[Data] is
val fOfData: F[Data] = [B <: Data] => (data: B) => if (data.n == 1) Right(data) else Left("not one")
Anyway, thank you very much for coming this far with me. It has been of great help.