3

I'm wondering how the type of the Stream below is casted?

type NTy = BigInt
def streamFib(n: Int): NTy = {
  lazy val fibs: Stream[NTy] = 1 #:: 1 #:: fibs.zip(fibs.tail).map(n => n._1 + n._2)
  fibs.drop(n - 1).head
}

This compiles and streamFib returns a BigInt type (as expected?).

I also noticed a few more behaviors:

val s1:Stream[BigInt] = 1 #:: 2 #:: Stream.empty // incorrect, found Int required BigInt
val s2:Stream[BigInt] = 1 #:: 2 #:: Stream.empty[BigInt] // correct
val l1:List[BigInt] = 1 :: 2 :: List.empty[BigInt] // incorrect, found Any required BigInt
val l2:List[BigInt] = 1 :: BigInt(2) :: List.empty[BigInt] // correct
Hongxu Chen
  • 4,862
  • 1
  • 39
  • 79

1 Answers1

3

tl;dr:

fibs is defined as a Stream[BigInt], so when you prepend an Int to it (1 #:: ...), the compiler looks for an implicit conversion from Int to BigInt and finds it in BigInt.int2bigInt



There are a couple of things going on here.

1) The BigInt companion object defines an implicit conversion from Int to BigInt:

implicit def int2bigInt(i: Int): BigInt = apply(i)

This means that wherever you need a BigInt you can supply an Int and the implicit conversion will convert the value. You can also say that Ints can be viewed as BigInts.

2) methods that end with a colon are right-associative. This means that 1 #:: 2 #:: Stream.empty[BigInt] can be effectively rewritten as Stream.empty[BigInt].#::(2).#::(1)


Now, if you look at the signature of Stream.#:: (def #::(hd: A): Stream[A]) you'll see that Stream[BigInt].#::(x) can only compile if x is a BigInt.

When you call 1 #:: 2 #:: Stream.empty[BigInt] you are calling Stream[BigInt].#:: passing an Int value instead of a BigInt, but, as I mentioned earlier, Ints can be viewed as BigInts, so they are automatically converted to BigInts and everything compiles.


When you do this: val s1:Stream[BigInt] = 1 #:: 2 #:: Stream.empty you are doing a different thing instead: you are creating on the right hand side a Stream[Int] (Stream.empty type is inferred to be Int from the 1,2 values you pass) and then you are assigning this value to a Stream[BigInt] val.

Unfortunately, there is no implicit conversion from Stream[A] to Stream[B], even if A can be viewed as B, thus compilation fails.

You can define your own implicit conversion though:

implicit def asBigIntStream(xs: Stream[Int]): Stream[BigInt] = xs.map(BigInt.int2bigInt)
val s1:Stream[BigInt] = 1 #:: 2 #:: Stream.empty //This now works

There's something else going on with Lists: differently from Stream, the List cons is defined as:

def ::[B >: A] (x: B): List[B]

With Stream.#::(x) you needed x to be the exact same type as the Stream you were prepending x to. With List.::(x), instead, x (that has type B) can be an instance of a supertype of the list's type. The resulting list will be a List[B], i.e. prepending to a list can widen its type.

So, when you do 2 :: List.empty[BigInt] you're effectively Calling List[A].::(x: B) where A is BigInt and B is inferred to be Any because Any is the most strict supertype of BigInt that is also a supertype of Int.

Since this makes the compiler happy, no implicit conversions are looked for, and the resulting list is a List[Any] that you can't use anymore as a list of integers.

You can basically call whatever :: List[X] to get a List[Y] where Y is the most strict supertype of both X and the type of whatever


So, why doesn't val l1:List[BigInt] = 1 :: 2 :: List.empty[BigInt] work while val l2 : List[BigInt] = 1 :: BigInt(2) :: List.empty[BigInt] does?

It's because of type inference. Let's rewrite the two expressions removing the right-associativity:

val l1: List[BigInt] = (List.empty[BigInt].::(2)).::(1) // incorrect, found Any required BigInt
val l2: List[BigInt] = (List.empty[BigInt].::(BigInt(2))).::(1)  // correct

I'm not 100% sure of this (anyone please correct me if I'm wrong):

  • The compiler can help the type inference only on the last application of :: (List.empty[BigInt].::(2)) is a List[Any] well before applying .::(1) so there's nothing we can do

  • (List.empty[BigInt].::(BigInt(2))) is already a List[BigInt] and the compiler can try to make .::(1) a BigInt (thus looking for implicit conversions)

Giovanni Caporaletti
  • 4,996
  • 2
  • 24
  • 37
  • You skipped the most interesting case `val l1:List[BigInt] = 1 :: 2 :: List.empty[BigInt]` Why, for streams, implicit conversion kicks off but for lists it doesn't and results in `List[Any]`? – Łukasz Apr 23 '16 at 08:38
  • @Łukasz It took me some time to write the answer, this question is far more interesting than I thought at the beginning. I could be wrong about a couple of things but I think more or less that's it! Take a look at it and tell me what you think, feel free to edit :) – Giovanni Caporaletti Apr 23 '16 at 08:48
  • Yes, I think now you covered all there was to it. I never noticed before that stream does not widen the type when appending, now what happens is clear :) – Łukasz Apr 23 '16 at 09:03
  • I didn't as well! that's why I think this question is interesting :) – Giovanni Caporaletti Apr 23 '16 at 09:05
  • For `s1` I was wondering why it cannot be inferred since `val s:Stream[BigInt] = Stream.empty` actually compiles (not quite into type inference theory:- – Hongxu Chen Apr 23 '16 at 10:15
  • Another problem is `what's the opportunity when implicit conversion is applied`? For example, `int2bigInt ` exists in BigInt.scala but not *import*ed in my module (e.g., `import scala.math.BigInt.int2bigInt`); I think you are absolutely here (e.g., `BigInt(1)+2` will get `BigInt(3)`) but I am personally confused about the specification. – Hongxu Chen Apr 23 '16 at 10:26
  • still, why `streamFib` actually compiles and gets BigInt type? – Hongxu Chen Apr 23 '16 at 10:30
  • @HongxuChen streamFib is a `Stream[BigInt]` (as per its own definition) and you prepend `Int`s to it that are implicitly converted to `BigInt`s, it's the same thing. I added it in the "tl;dr". Look at [this question](http://stackoverflow.com/questions/5598085/where-does-scala-look-for-implicits) for the implicit resolution rules. The implicit conversion is defined in BigInt's companion object, you don't need to import it – Giovanni Caporaletti Apr 23 '16 at 10:38
  • @GiovanniCaporaletti what i'm mostly confused about is that `Stream.empty` in `s1` cannot be inferred as `BigInt`. is there any spec about this? a quick glimpse of http://www.scala-lang.org/files/archive/spec/2.11/03-types.html doesn't help. – Hongxu Chen Apr 23 '16 at 11:35
  • You are first evaluating the RHS (`Stream.empty.#::(2)` etc) and this leads to a Stream[Int]. I guess the compiler doesn't look at the LHS to build a successful RHS if it doesn't need to. With `l2` is different because you are prepending an `Int` to an already existing `List[BigInt]`, so implicit conversion kicks in. Driving types from the LHS is a bit tricky in scala, I'm really not sure about this but I think the RHS type is evaluated first, then the compiler tries to assign it to the val. I guess some scala compiler expert will be able to tell you more about this – Giovanni Caporaletti Apr 23 '16 at 11:50