2

I am learning Scala and found the following:

List(('a', 1)).toMap get 'a'           // Option[Int] = Some(1)
(List(('a', 1)).toMap) apply 'a'       // Int = 1
(List(('a', 1)).toMap)('a')            // Error: type mismatch;
                                          found   : Char('a')
                                          required: <:<[(Char, Int),(?, ?)
                                          (List(('a', 1)).toMap)('a')

But then assigning it to a variable works again.

val b = (List(('a', 1)).toMap)
b('a') // Int = 1

Why is this so?

The standard docs gives:

ms get k

The value associated with key k in map ms as an option, None if not found.

ms(k) (or, written out, ms apply k)

The value associated with key k in map ms, or exception if not found.

Why doesn't the third line work?

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
Lieu Zheng Hong
  • 676
  • 1
  • 10
  • 22
  • 1
    There are many things happening here. First, your examples are incorrect. `get` as the documentation explains, returns an **Option** while `apply` returns a plain value. If the key does not exists, `get` will return an **None**, whereas `apply` will throw an **Exception** - Second, the problem is that, `toMap` receives some implicit arguments. That is why it is better to split the code in two lines, because it confuses your `apply` with passing those arguments. – Luis Miguel Mejía Suárez Aug 06 '19 at 16:34
  • 1
    @LuisMiguelMejíaSuárez Have corrected the examples, thanks for pointing this out. Could you please explain the "implicit arguments"? – Lieu Zheng Hong Aug 06 '19 at 16:41
  • 2
    @LieuZhengHong It's a good question, and the behavior really is counter-intuitive. Does [this duplicate](https://stackoverflow.com/questions/50259606/scala-apply-method-call-as-parentheses-conflicts-with-implicit-parameters) answer your question? Essentially, it attempts to treat `'a'` as an implicit argument to `toMap`. This here works: `(List((1, 2)).toMap(implicitly[(Int, Int) <:< (Int, Int)]))(1)`. This also works: `List((1, 2)).toMap.apply(1)`. But with just the parens, it doesn't work, for questionable reasons. – Andrey Tyukin Aug 06 '19 at 16:51

2 Answers2

5

It's essentially just an idiosyncratic collision of implicit arguments with apply-syntactic sugar and strange parentheses-elimination behavior.

As explained here, the parentheses in

(List(('a', 1)).toMap)('a')

are discarded a bit too early, so that you end up with

List(('a', 1)).toMap('a')

so that the compiler attempts to interpret 'a' as an implicit evidence of (Char, Int) <:< (?, ?) for some unknown types ?, ?.

This here works (it's not useful, it's just to demonstrate what the compiler would usually expect at this position):

(List(('a', 1)).toMap(implicitly[(Char, Int) <:< (Char, Int)]))('a')

Assigning List(...).toMap to a variable also works:

({val l = List((1, 2)).toMap; l})(1)

Alternatively, you could force toMap to stop accepting arguments by feeding it to identity function that does nothing:

identity(List((1, 2)).toMap)(1)

But the easiest and clearest way to disambiguate implicit arguments and apply-syntactic sugar is to just write out .apply explicitly:

List((1, 2)).toMap.apply(1)

I think at this point it should be obvious why .get behaves differently, so I won't elaborate on that.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
1

The signature is slightly different:

abstract def get(key: K): Option[V]

def apply(key: K): V

The issue is error handling: get will return None when an element is not found and apply will throw an exception:

scala> Map(1 -> 2).get(3)
res0: Option[Int] = None

scala> Map(1 -> 2).apply(3)
java.util.NoSuchElementException: key not found: 3
  at scala.collection.immutable.Map$Map1.apply(Map.scala:111)
  ... 36 elided

Regarding the failing line: toMap has an implicit argument ev: A <:< (K,V) expressing a type constraint. When you call r.toMap('a') you are passing an explicit value for the implicit but it has the wrong type. Scala 2.13.0 has a companion object <:< that provides a reflexivity method (using the given type itself instead of a proper sub-type). Now the following works:

scala> List(('a', 1)).toMap(<:<.refl)('a')
res3: Int = 1

Remark: i could not invoke <:<.refl in Scala 2.12.7, the addition seems to be quite recent.

lambda.xy.x
  • 4,918
  • 24
  • 35
  • Thanks for the answer. I have made the question more specific. I understand the difference between Option vs throwing exception. I suppose my main confusion is why assigning to a val works, but putting it all on one line doesn't. – Lieu Zheng Hong Aug 06 '19 at 16:44
  • I just realized that `toMap` has an implicit argument: `def toMap[K, V](implicit ev: <:<[A, (K, V)]): immutable.Map[K, V]` which provides a witness object for type coercion. Scala tries to call that one instead of `apply`. I haven't found out yet how to create a default witness to pass. Then it should work by calling `List(('a',1)).toMap(ev)('a')`. – lambda.xy.x Aug 06 '19 at 18:18
  • Added the explanation – lambda.xy.x Aug 06 '19 at 21:14
  • 1
    Thanks for `<:<.refl`, didn't know about that. – Andrey Tyukin Aug 07 '19 at 14:19
  • According to the git history, it was added in December 2018, it might only have recently surfaced in the release version. – lambda.xy.x Aug 07 '19 at 14:24