Pre-SIP: A Syntax for Collection Literals

We could, but IMO [].toMap works just was well and does not require special syntax.

1 Like

Python also had the problem with zero element sets colliding with dicts, and they just decided {} is an empty dict snd you use set() for the empty set. In practice this isn’t a problem at all.

For Scala, that would mean using Map() for empty maps in the absence of target typing. Places with expected types we can just infer/instantiate the correct type of collection without boilerplate or ambiguity. I would expect the occassional case where an explicit collection constructor to be required won’t be a problem either

2 Likes

It’s not a problem per sĂ©, but it would be nice that, if we introduce a new ruleset, it does not come with exceptions at conception. See the trouble we have with the regularisation of the definition of the Tuple. It hurts orthogonality.

Further, and probably this has be said before, we have come this far without a syntax for collection literals. Do we really expect that introducing it would attract a whole new poule of users? I think not. Especially not, when it starts out with exceptions to the rule.

7 Likes

One year on - I have implemented the proposal in scala-object-notation io.github.bishabosha::scala-object-notation::0.4.3

here is a sample file using the syntax to define maps and sequences:

3 Likes

@bishabosha @odersky i think it looks pretty good! It would be nice if we could get this notation into Scala itself in a typesafe manner rather than as an external DSL

1 Like

I think the real dispute is just notation for guaranteed type inference of “empty map”, and to that perhaps settling for Map() is not so bad

Personnally I’m still not convinced that:

  1. This is needed
  2. This is the best way to do it

^^’

Also note that scala-object-notation (while very cool) is devoid of type parameters, so it can be very easy to parse visually, while Scala+[] might not

P.S: What’s the json of scala-object-notation ? scon? salon? scalaon?

well the idea is meant to be 1:1 so that you can compile it as part of source code, or parse it programatically. So for me it would not be satisfactory to have SON not actually be valid Scala code - so this compact form would stay in some flag limbo.

lets turn to some python code:

x: list[int] = [1,2,3]

ideally .scala or .sc but if this compact form becomes popular and no one want to accept collection literals in the main language then i guess introduce new syntax highlighters and use .scon

Yes yes of course, my “This is needed” was talking about the feature, not scala-object-notation
And I don’t think “This project needs it to be merged” is a strong argument for a feature
(or rather it depends on the project, but that one is very new)


And furthermore, python does a lot of dumb things we shouldn’t do ^^’

(my favorite being:

a = -1
b = -42

match (0, 42):
  case (a, b) if a != 0:
    # unreachable
    pass
  case _:
    assert a == 0 # not -1 !
    assert b == 42 # not -42 !

)


I wasn’t even thinking that far haha, “scala-object-notation” is just a bit long to type
(in the same way no one writes “Java Object Notation”, they just say “json”/“JSON”)


Also note that I am more unconvinced by “2. This is the best way to do it” than by “1. This is needed”
Number 1. because it feels like something that might be useful
Number 2. because to me this feels like the worst way of going about it ^^’
Valuable work was done to split _ into _ and ?, and that seems much more niche of a confusion than [] for type params vs for collection litterals
And I know they feel super different for us, but foo[1] and foo([1]) are really similar !


But I’m not sure how to resolve the disagreement, a lot of arguments from all sides have already been stated, so its unlikely additional ones will change anything

2 Likes

ok, i can see the case that for uninitiated it would be less immediately obvious the difference between type application and value arguments on a method.

perhaps a similar word could be said for the classic puzzler foo(x = 23) vs foo({x = 23}) (or even foo{x = 23})

this was very entertaining btw so thank you :ok_hand:

1 Like

We can combine the two !

foo[(x = 23)] vs foo([{x = 23}]) vs foo[{val x: 23}] vs foo({x = 23})

(Not necessarily a strong argument, altough it does maybe hint that symbol soup might be more likely)

1 Like

i would hope good taste prevails on a case-by-case basis . e.g. bulk/config-style data = ok, extreme nesting that isnt self-explainatory = no, please make an alias,

really feels like something that would be sorted in code review. e.g. foo([1]) is bad because foo itself is a bad name. wheras np.array([1]) is somehow ubiqitous.

and say Scala ever introduced vararg type parameters, well then whats so unreadable with array[1,2,3] vs array([1,2,3]) ?, i would say understanding is about reading the surrounding context, and not getting lost in details such as “is this a type argument or not”.

i think that the proposal should be split to 2 stages to increase appeal:

  • stage 1) target typed collection literals, where there are no ambiguities, because the collection types exist before parsing collection literals
  • stage 2) collection literals without target typing requirement. this would have some ambiguities, ‘magic’, complexities (especially when combined with overloading), etc

stage 1) doesn’t give all benefits, but also doesn’t bring all disadvantages.

stage 2) could be done after stage 1) if and when people are ready for it. i think that, if done carefully, doing stage 2) after stage 1) could give full advantages of unrestricted collection literals, but be less disruptive.

my opinion is that stage 1) would bring vast majority of benefits with minimal controversy. then stage 2) would be considered unneeded.

the 2-dimensional matrix was a frequent example in this topic. if defining such matrix in hypothetical syntax for unrestricted collection literals looks like:

val myMatrix2 = (
  (0, 1, 2),
  (3, 4, 5),
  (6, 7, 8),
)

then with target typed collection literals we can have a common helper method:

def matrix2[T](rows: Array[T]*): Array[Array[T]] = rows.toArray

that we can then use to have target typing to disambiguate collection types, so final code with target-typed collection literals would be:

val myMatrix2 = matrix2(
  (0, 1, 2),
  (3, 4, 5),
  (6, 7, 8),
)

whether such examples are representative of most potential applications of collection literals in scala ecosystem is unclear. i guess that matrices often want to be raw arrays, but common collection usages want to be something higher level (i.e. seq, set, etc), so having matrices as the forefront example for unrestricted collection literals is kinda unfitting.

2 Likes

i vote to keep the magic :slight_smile: observe swift magic

$ swift repl
Welcome to Apple Swift version 6.3.2 (swiftlang-6.3.2.1.108 clang-2100.1.1.101).
Type :help for assistance.
  1> [1,2,3]
$R0: [Int] = 3 values {
  [0] = 1
  [1] = 2
  [2] = 3
}
  2> let ints: Set<Int> = [1,2,3]
ints: Set<Int> = 3 values {
  [0] = 3
  [1] = 2
  [2] = 1
}

these are simple examples. what will happen if we combine that with overloading, high degree of polymorphism, complex nesting, etc? what will be the cognitive load then? how predictable would it be (i.e. how it would respond to refactoring of called methods or adding new machinery in nearby scope)? how complex the disambiguation rules would be?

for comparison, scala allows for unrestricted operator definitions. this is an extreme position compared to e.g. java (which has only built in operators, mostly for primitives and strings), c++ (you can define your own operators, but available names for operators are strictly restricted), etc. historically, that resulted in abuse of operators in many libraries and tools, e.g. scalaz and sbt. scala community learned a lesson from it and now is keeping operators to sane minimum. we should be more cautious this time.

i don’t know swift, but i guess random swift code is on average less syntactically tricky than random scala code. we should manage the cognitive load budget carefully.

the problem with simple examples that look more impressive with unrestricted collection literals is also that the full syntax (without collection literals) is still simple and compact. there’s not a big difference between occasional Seq("ala", "ma", "kota") and ["ala", "ma", "kota"]. therefore, we should find better examples.

i think if we implement stage 1) (i.e. only target typed collection literals), and wait for scala ecosystem to adopt it, then we will have much more code that could be mechanically checked to see if dropping target typing requirement will bring enough benefits to consider stage 2) (unrestricted collection literals).

4 Likes

I can assure you that 99% of the programming community has no issue with these two things looking similar. Scala certainly wouldnt be the first language that allows those two syntaxes! They are, i may dare say, some of the most common syntaxes in the entire programming world.

well here’s a mess you can already do today:

$ scala --preview
scala> import scala.Conversion.into

scala> given [K,V] => Conversion[Seq[(K, V)], Map[K, V]] = Map.from(_)
def given_Conversion_Seq_Map[K, V]: Conversion[Seq[(K, V)], Map[K, V]]

scala> def foo(x: into[Map[String, String]]) = ()
def foo(x: into[Map[String, String]]): Unit

scala> foo(Vector(("abc", "def")))

scala> @annotation.targetName("fooI") def foo(x: into[Map[String, Int]]) = ()
def foo(x: into[Map[String, Int]]): Unit

scala> foo(Vector(("abc", "def")))
-- [E007] Type Mismatch Error: -------------------------------------------------
1 |foo(Vector(("abc", "def")))
  |    ^^^^^^^^^^^^^^^^^^^^^^
  |    Found:    Vector[(String, String)]
  |    Required: Conversion.into[Map[String, Int]]
  |
  | longer explanation available when compiling with `-explain`
1 error found

so is a default type inference of ["abc" -> 123] introducing a new kind of situation that doesnt already exist today, no

I’m not so sure, most languages do not have this ambiguity because they do at least one of:

  1. Don’t allow 1 as a type
  2. Do not use []for types
  3. Do not use [] for collection literals

Python does 3, and 2 only partially (no def foo[T](x: T))

Never and Nothing are the most popular names for the bottom type, yet allowing both would make the system more confusing!
What I mean by that is that both semantics of [] are perfectly fine and understandable on their own, it’s combining them in the same language which is an issue

Also if we want to keep “Scala is the best language for learning”, then we also really need to consider that remaining 1% !

2 Likes

I mean, that’s a bit disengenuous, we do not recommend using Vector(("abc", "def")) as a way to make Maps, whereas the goal of ["abc" -> 123] is to make a Map !

here i am pointing out this overloading issue already exists for any mix of overload and DSL that relies on target-typing.

I specifically used Vector[(X, Y)] as well yeah that would be the type that [x->y] syntax produces (actually Seq). (unless target type is different)