Union type constructor's parameter inferred to be Any

Hi there, everybody!

The compilation of the following piece of code

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

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.

Oh… Now I see the conclusion that A <: Data does not follows due to F and G contravariance :disappointed:

But still interested in knowing whether it is possibly remove the need to explicitly declare mustReturnF type parameter.

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

Don’t worry, I appreciate you sharing your point of view and won’t take it personally.

Can’t speak for others, but in this case my motivation is that if we remove G[_] from the type union of consumesFOrG like

def consumesFOrG[B <: A](justF: F[B])

Then the compiler is able to infer the type of Data to returnF. So I think it is reasonable to ask why this is not the case with the type union.

And thank for the reading suggestion! Will take a look.

It looks like Dotty does not infer union types by default scala - How does Dotty decide how to infer/when to widen Union Types? - Stack Overflow it seems to be a design decision. The person asking the question has a similar situation as you, in one case it infers Any and in another case it infers correctly.

Here are the relevant parts from the documentation and Github issues: https://github.com/lampepfl/dotty/pull/2330 https://github.com/lampepfl/dotty/issues/4867 https://github.com/lampepfl/dotty/issues/6565 Union Types

It says:

The compiler will assign a union type to an expression only if such a type is explicitly given.

Later it says in Union Types - More Details :

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.

Well, Haskell does not have inheritance, which makes it’s type-system quite different…

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.

@mucciolo Is it even possible to define a value of type F[something], say F[Data] in the REPL? I’ve been trying for an hour and I can’t do it.

type F[-A] = [B <: A] => B => Either[String, B]
case class Data(n: Int)
val myFun: F[Data] = (d1: Data) => (d2: Data) =>
  if d1.n == d2.n then Left("") else Right(d2)

scala> :l a.scala
-- [E007] Type Mismatch Error: ---------------------------------------------------------------------------------------
6 |val myFun: F[Data] = (d1: Data) => (d2: Data) =>
  |                     ^
  |                     Found:    Data => Data => Either[String, Data]
  |                     Required: F[Data]
7 |  if d1.n == d2.n then Left("") else Right(d2)
  | Explanation (enabled by `-explain`)
  |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  | Tree: {
  |   def $anonfun(d1: Data): Data => Either[String, Data] = 
  |     {
  |       def $anonfun(d2: Data): Either[String, Data] = 
  |         if d1.n.==(d2.n) then Left.apply[String, Nothing]("") else Right.apply[Nothing, Data](d2)
  |       closure($anonfun)
  |     }
  |   closure($anonfun)
  | }
  | I tried to show that
  |   Data => Data => Either[String, Data]
  | conforms to
  |   F[Data]
  | but the comparison trace ended with `false`:
  |   ==> Data => Data => Either[String, Data]  <:  F[Data]
  |     ==> Data => Data => Either[String, Data]  <:  [B <: Data] => (B) => Either[String, B]
  |       ==> Data => Data => Either[String, Data]  <:  PolyFunction
  |       <== Data => Data => Either[String, Data]  <:  PolyFunction = false
  |     <== Data => Data => Either[String, Data]  <:  [B <: Data] => (B) => Either[String, B] = false
  |   <== Data => Data => Either[String, Data]  <:  F[Data] = false
  | The tests were made under the empty constraint
1 error found

Anyway, I think I’m giving up :white_flag: , sorry bye :wave:

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.

1 Like