Pre-SIP: a syntax for aggregate literals

The argument wasn’t about syntax in general, it was about redundant syntax, which doesn’t provide any expressiveness gain.

I very much prefer a language like Scala to say LISP or Haskell exactly for the reason that Scala has rich syntax and is keyword based. This makes scrolling though code so much easier! You don’t need to read carefully all the words on screen just to recognize different functional parts in the code. Especially in S-expression soup it’s really difficult to see anything. Even it has “very simple syntax”.


@Ichoran summarized the imho perfect solution in his previous post.

I still think the apply proposal is a little bit “too magic” (even I have no issue with magic in Scala in general). But one would achieve the same with the type-class (and likely target typing) approach, just in a more controlled manner. It would add expressiveness to the language: You could “spread” tuples into some target type if you have the proper capability (type class) for that in scope.

Type-classes optionally enable features, and that’s the “usually” way to activate “magic” in Scala.

Having to use the spread operator would prevent the usual gotchas with implicit conversions.

So this would be a very controlled feature. No magic where it’s unexpected! (Which is imho the main issue with the apply approach: It could trigger anywhere, as it would be always globally enabled; in contrast to the more controlled approach).

But both approaches share the property of adding a kind of powerful new feature to the language. Not just redundant syntax for Seq and Map, which is imho plain useless.

(Named)Tuples would become universally usable where some other types are expected, just by bringing a type-class instance into scope. Which would be safer than using implicit conversions, and simpler than using tuple type level programming to achieve the same convenience feature!

I think code like

perlinNoise(
  seed = 0,
  sideLength = 512,
  layers =
    (frequency = 2 , persistence = 0.5  )*,
    (frequency = 10, persistence = 0.25 )*,
    (frequency = 20, persistence = 0.125)*,
    (frequency = 40, persistence = 0.125)*
)

looks really well.

For common cases like converting from (named)tuple to case class the compiler could provide the needed type-class instances, so just a simple import or some other one-liner would be sufficient.

But one could still use this feature to represent other types with shorthand syntax, which was core of the original idea. But to activate that magic one would need to write some type-class instance. I think that’s fair, a good balance of safety against surprises and ease of use. (Very similar to implicit conversions, actually!)

Regarding syntax once more: One needs to look at it in context. I think that’s very important. Like @megri said, if you have only List / Array and Map (which are in PHP even the same thing…) having dedicated syntax for that is indeed very natural. I think I said already that this (late!) syntax addition was a big win in PHP. But it simply wouldn’t be in Scala where you have much richer types.

Also any “familiarity” argument makes imho no sense. Beginners don’t know any syntax yet. For a beginner it’s very important that the syntax is logically consistent and unambiguous, but what it is concretely isn’t really important.

For people coming form other languages it also doesn’t matter. Scala is Scala, and not any other language. We have already some different syntax in contrast to some currently more mainstream languages, like […] instead of <…>, or value: Type instead of type value. But regarding the last example the fashion just changed, since Rust and Kotlin (and maybe Scala). If Scala would have picked up the most “familiar” syntax back than it would look like C++ / Java, right? But now it’s like Python? Or is it Rust?

Again internal consistency is more important here then mimicking any other language just because it’s currently more fashionable! Otherwise you end up like PHP, which is a Frankenstein monster of mixed syntax cobbled together from everywhere, exactly with the argument that it’s already “typical syntax for that feature”!

Especially as such trends come and go, and different languages have anyway different solutions. So for someone coming from Java or C++ using […] for sequences would be very alien as the mentioned languages use {…} for that (and I’ve heard rumors quite some people come from this Java thingy, no clue).

So this “familiarity” argument isn’t very strong really. (Except our target audience are JavaScript, Python, PHP, and Perl developers… Oh, and I forgot Swift. Apple developers are an easy target for Scala, sure. :joy:)

The situation in Scala would be more like the sequence literal from C++ which can instantiate richer types than Array. But this feature there is regarded more problematic than helpful… Especially as it is also “overloaded” syntax ({}) so it’s hard to parse without looking at the context. Same would be the case for the confusing misuse of type syntax: Currently one can scroll though large batches of code and instantly recognize type parameters. By “overloading” the syntax this would be gone. (Syntax highlighting would help I guess, but still harder to parse visually).

Going the @Ichoran route instead of just adding some—in the context of Scala—very weird syntax just for Seq and Map would also include that other new (and likely exciting) feature @Odersky proposed earlier when discussing the type-class approach.

All in all this would look very good to me:

We get a new feature (“spreading” (named)tuples into other structured types), which would be still quite safe even a little bit “magic”, it would at the same time upgrade (named)tuples to an even more useful and powerful feature (nice and simple interaction with things like case classes and collections), and it would be a user extensible feature (through type classes) where you need the full magic power of “tuples as basic data structure literal for everything” (kind of like in the original proposal from @mberndt—just with a syntax that makes much more sense in the context of Scala, and no automatic like the old implicit conversions).

Also I think tooling would have less issues with a “recycled” “spread” syntax on tuples than changing fundamental things, like that […] is reserved for types, which I guess was a strong assumption and is somewhere deeply backed in. (Just speculating, don’t know how that for sure)

4 Likes

Totally agree, for me that is potentially the most important design requirement for both the Scala language and Scala code.
(Okay, potentially correctness comes first …)

But … I don’t really agree with this reasoning:

  1. You do not need to understand apply methods and varargs to understand that List(...) creates a list of the elements, maybe except in cases like List(3) (singleton containing 3 vs default-initialized list of length 3)
    As a related example, in C++, you do not need to understand the details of what an aggregate is to understand Person john = {"John", "Snow"};
    (I’m looking here not at the syntax, but at the features used, as this is part of the argument)
  2. I do agree that in particular Seq(...) and Map(...) are somewhat confusing, but for me this is due to the fact that the terms Seq and Map are a lot less usual than for example List or Dict (both in programming and non-programming contexts) !
  3. If we really follow through with this, maybe we should disallow type parameters and arguments in scripts ?
    Since people unfamiliar with Scala might understand f[1,2] as shorthand for f([1,2])

As others have pointed out, this is a very bad way to measure ease-to-learn, for example due to platform effects
(The more people use it, the more attractive it is)

A potentially better way is to look at people learning Scala in programming courses, and my experience has been that people have a very easy time learning it, even as a first language
(But of course a Scala teacher would probably be the person to ask, more-so than a student who learned alongside others)

Overall if we really want to have something like this for me there are two options:

  1. Generalizing the spreading operator (...)*
  2. Going all the way with syntactic sugar, and go back to the original proposal, or to one of its variants. My favorite being .x with companion obect inference, generalising to .(...) which would desugar into an apply method, for example: List.apply(...)

Anything else is a strange tradeoff between usability, cohabitation with existing syntax and conformance with other programming languages.
In particular, a bracket-based syntax I foresee bringing us worlds of pain

But we are arguing in circles, we each have nothing more than our aesthetic criteria (forged in principle and experience nonetheless)
To resolve the question of beginner-friendliness it we need data, either from a Scala developers survey as some have proposed, or from on-the-ground research querying Scala beginners both with and without programming experience (knowing full well that field is in its infancy)

5 Likes

I don’t think it’s only aesthetic, actually. There are objective differences between the proposals:

  • versatility: how many different situations can I use the feature in
  • re-use of existing stuff, like existing apply methods on companion objects
  • orthogonality
    • choice between syntactic variants like Seq(…) or […] shouldn’t affect either semantics or performance

And there’s probably more.

It occurred to me recently that it’s actually fairly simply to limit the “[…] as companion object apply” idea to cases that the author considers desirable (since this is something that @odersky is so concerned about). I suggested earlier that we could make it possible to override which type’s companion object the […] syntax should refer to:

If we want this feature to be limited and harder to abuse, we could simply always require such a definition. Then it would be as safe as the typeclass based proposal while simultaneously being much simpler and orthogonal.

1 Like