Pre-SIP: a syntax for aggregate literals

Ah good question. I was thinking of the scenario where we had some target typing, so if we expected a Map then it would become Map("k1" -> "v1", "k2" -> "v2"). I didn’t consider the standalone scenario without it.

I’m not in favor of this because it clashes with other features:

val a = 0
val b = 1

[a = ..., b = ...] // Map(0 -> ..., 1 -> ...)
// vs
(a = ..., b = ...) // (a = ..., b = ...)

In one case, the lhs of the equal is a variable to be de-referenced, in the other it is a name to be used as-is
(There might be cases with this kind of problem outside of this proposal, but I think we should avoid adding to them)

In particular since we have the arrow to create tuples
(Or rather maps, I never use it for anything else)

1 Like

I think we can re-use ["k1" -> "v1", "k2" -> "v2"] as the constructor for maps.

  • If there’s a target type, it will have an apply method that takes a vararg of pairs, so everything works out.
  • If there’s no target type, we make a special case that if the constructor is an aggregate literal of pairs constructed from -> (the one in Predef.arrowAssoc) we produce a Map instead of a Seq.

To further reduce surprises I would like to try out a new typing concept, let’s call it a manifest target type (or in the terminology of the Scala Spec, a a manifest expected type). This is the expected type where we do follow aliases but do not follow type parameter instantiations or abstract type bounds. For instance, in

type IntMap[T] = Map[Int, T]
val x: (IntMap[String], Boolean) = ([1 -> "I", 5 -> "V", 10 -> "X"], true)

IntMap[String] is a manifest target type so we allow a literal. On the other hand, in

def f[T](xs: T): Unit = ...
f[IntMap[String]](IntMap[1 -> "I", 5 -> "V", 10 -> "X"]) // error

IntMap[String] is not a manifest target type, so a second explicit IntMap constructor is needed. If course, if IntMap[String] was inferred it would not be a manifest target type either.

“Manifest target type” is a new spec and implementation concept, and we need to to investigate it further to see whether it pans out. But I believe it would be useful here, and possibly in other situations as well.

The first variant of my proposal works for arrays: Anything that has a vararg apply method can be inferred, and both Array and IArray have such a method. If we restrict the inferred constructors we would indeed have to accept Array and IArray in addition to Seq.

I would like to focus first on sequence (and map) literals. We can revisit more general object literals afterwards. As I wrote before it’s a slippery slope and there are lots of downsides.

I think the named tuples solution is simply not good enough – we should strive to provide a feature that provides as much useful functionality as possible with the code bases that exist today, which means that it must work smoothly with case classes.

Maybe this can be solved by allowing named tuples to be automatically converted to case classes instances when an expected type is present
Very much like what we do with SAM-conversion

1 Like

I’m afraid it wouldn’t be that simple. Consider this case:

val x: Map[Int, List[Int]] =
  [42 -> [1,2,3]]

That would be rewritten to

val x: Map[Int, List[Int]] =
  Map(42 -> [1,2,3])

And that would need to be rewritten further to

val x: Map[Int, List[Int]] =
  Map(42 -> List(1,2,3))

But you can’t do that because -> will accept any type at all, so the compiler has no way to know that the expected type is List[Int]

Note that in today’s Scala we already have a case where the expected type is used: lambda expressions. When the expected type is known, the type of a lambda parameter can be left out, but -> breaks that. The following compiles:

val x: (Int, Int => Int) = (42, _ + 1)

But this does not:

val x: (Int, Int => Int) = 42 -> _ + 1
-- [E081] Type Error: ----------------------------------------------------------
1 |val x: (Int, Int => Int) = 42 -> _ + 1
  |                                 ^
  |                           Missing parameter type

So we need to either make the “expected type” thing smarter than it is today, or make -> effectively its own syntactic form instead of an extension method.
Note that this is actually a separate problem that doesn’t have anything to do with aggregate literals, as it occurs with uses of -> outside of collections as well.

Sorry, but I’m going to have to push back on this as it’s inconsistent with how the language works today. List[Int => Int](_ + 1) is valid code today, and I think the expected type stuff should work the same for aggregate literals as it does for lambdas.

That is effectively the same as simply changing the syntax for object aggregates to use () rather than [] – which is probably a good idea as it reduces the number of syntactic forms in the language.

I would not like a general implicit conversion from named tuple types to case classes, i. e. one that also works for expressions other than literals.

A lot of things do not work with wildcards, since the compiler does not know if you want for example:

 x => (42 -> x + 1)
// or
 42 -> (x => x + 1)

Writing the function explicitly does work:

I therefore see no reason the following would not work:

This use case is covered by NamedTuples, which can be lifted/converted to whichever domain class you need

Edit: I see many have suggested this above

Actually it doesn’t have anything to do with wildcards and everything to do with the fact that I messed up the parens. val x: (Int, Int => Int) = 42 -> (_ + 1) compiles just fine, note the parens around the wildcard expression.
Anyway, your point still stands, I was wrong about the whole -> thing. So I agree with @lihaoyi: We don’t need special rules for Map literals. I think that it’s just fine to simply spell out the collection type in the cases where no expected type is known. And if that really turns out to be too much of a burden, I think the solution isn’t to add special cases for Seq and Map but to improve the type inference algorithm. I. e. why would

foo(_ + 1)

work but

val x = _ + 1
foo(x)

not work? I think Rust allows this sort of thing, so we might want to reconsider the decision that type inference for variables can’t consider how the variable is being used later on.

1 Like

there is no cross-expression type inference in Scala, i.e. tracking the usage of x (to infer which SAM type it should be) is pretty wild/expensive/intractable to do, its easier if there is no sub typing such as in Haskell or rust

This would be a bit of a syntactic hack right? Since it would mean that val x = [1 -> 2] would have a different type than val x = [(1, 2)].

It’s a bit awkward, but I think I would support such a special case. Going back to the original motivation, there is a certain set of things that are worth special casing: “sequence literals” is definitely among them, and I think “dictionary literals” is probably a close second. So even if it’s a bit unusual I think it’s sufficiently teachable and intuitive that it’ll be OK

Even conceptually it makes sense that 1 -> 2 is a “map’s key value pair” whereas (1, 2) is a “tuple” even though they have the same type. The concepts are a bit fuzzy but I think it’ll work out ok

1 Like

I don’t think it’s a huge problem – just inline all val definitions and then run type inference. If the same inlined val has different inferred types in different positions, use the least upper bound greatest lower bound.

Anyway, the details of this are besides the point and I don’t want to derail the discussion with the details of this particular problem. My point is: I wouldn’t like to have some “default collection type” as that would preclude possible future improvements in this area, and that is too high a price for the marginal benefit that having a default collection type would provide. If the expected type is unknown, you simply type Seq or Map.

1 Like

Yeah this will likely never happen. Such a change will break type inference for approximately 100% of Scala programs out there. It’s actually hard to imagine any problem being huger than that. Let’s not discuss this further in this thread because it is off topic

1 Like

This all seems like a whole lot of complexity to introduce 2 ways (didn’t we want less ways of doing things?) of creating sequences and maps, saving 3 or 4 characters in most cases.

I don’t think this is really a fair comparison. In most of those languages they have special syntax only for creating low-level array(-like) datastructures.

By the way, from looking at the Kotlin docs their syntax seems to be

val simpleArray = arrayOf(1, 2, 3)

// Creates an Array<Int> that initializes with zeros [0, 0, 0]
val initArray = Array<Int>(3) { 0 }

// Creates an Array<String> with values ["0", "1", "4", "9", "16"]
val asc = Array(5) { i -> (i * i).toString() }
4 Likes

The current syntax is, but that may change soon. Hence my reference of KT-43871

Some of them. IIRC Swift, Kotlin with KT-43871, and C# all use target typing. We’re not talking about esoteric unknown languages here

@odersky we discussed varargs, and the Kotlin proposal does have something to say about that:

Collection literals give Kotlin a chance to address the long-standing design problem that the general creation of collections is currently inefficient in Kotlin due to the underlying use of varags.

I assume they’re considering desugaring collection literals into some kind of builder pattern. We might want to consider that for Scala as well, since that would avoid burdening Scala developers with the “concise collection literal is slow and verbose builder pattern is fast” dilemma that they suffer from today

1 Like