Pre-SIP: a syntax for aggregate literals

All of these statements are wrong.

  1. The false singleton is solvable with a trick that peeks at the source and if brackets are identified then treat it as a single element tuple.
  2. It’s not limited to collections. Any class is possible.
  3. There is no confusion with tuples. Can you give a specific example?
5 Likes

Even better news!

I managed to workaround the covariance implicit conversion issue. The fromtuple library can fully work without the ~[T] wrapper. I left the wrapper in as an option for those who don’t want to import the global tuple conversion.

Check it out!

9 Likes

Where’s the one-element list example in your tuple implementation? I didnt see one in your readme

It’s not yet implemented

Solved. Currently the implementation converts (1) and 1 just the same. It is possible to limit this to only when brackets are applied by checking the source, but since it could get a bit complicated due to possible whitespaces and comments I did not enforce it.

2 Likes

I am not sure tuples is the way to go. A conversion from (x) to C(x) where C is whatever collection you designate as a target looks like it is too powerful. Besides I think the main value of collection literals for beginners is that no target type is needed. [a, b, c] is a Seq and [a -> a1, b -> b1, c -> c1] is a Map. This makes the feature very beginner friendly.

Besides, if a tuple can be converted to a collection, and a singleton expression can also be converted to a collection, should not (a) also be convertible to Tuple1(a)? But there’s a reason we did not do this previously! We have learned that generally applicable generic conversions like these are troublesome.

1 Like

The most beginner friendly thing is to not have this feature because then they’ll use the thing that they need to learn about anyway: the companion object’s apply method.

2 Likes

I also am skeptical about the tuple-based syntax due to the interaction with grouping parenthesis for one-element lists, and the necessity of target types. But having a prototype implementation is nevertheless useful, as something we can re-use or compare future implementations against

5 Likes

Is it possible to somehow award some Hero badge to @soronpo?

The lib looks amazing! :heart_eyes:

If I have this I would never use any super odd and broken syntax like the proposed “collection literals”. I would actually instantly outlaw this features in any of my code bases. It’s just the next big wart in the language! Ugly, Irregular, and mostly useless as it’s to limited.

How is fromtuple “too powerful”? It’s based on type-classes, and not some compiler magic! Exactly like it should be.

(If there would be any compiler magic, than it should be at most using the “expansion operator” (*) to trigger the conversion.)

Finally working around the weird issue that there is no tuple syntax for one element tuples is also great! This was just an inconsistency. This here would solve it for good.

Also target typing is what makes this here safe. Target typing was until now one of the key points. Now people are arguing against it? What?

This here is much better than any previous ideas, and especially much much better than breaking the language with some completely alien and weird […] syntax!

2 Likes

That may be true in type unsafe languages like python or yaml. But In Scala land where such code is applicable, you would easily slip into unusable Seq[Any] or Map[String, Any], just because not all elements are the same type. And then, good luck finding the rogue element. IMO, in Scala, big literal lists and maps, especially when composed, should always have a target type. The fromtuple mechanism helps locate rogue elements easily. This is much more beginner friendly for Scala, IMO.

Regarding (a) not being a Tuple1(a), maybe we should reexamine this irregularity.

7 Likes

I don’t share the assumption that friendly features like aggregate literals are only valid in type unsafe languages like Python. Everyone can write [1, 2, 3], you don’t need a target type for that.

This may be true, but it’s a problem today already. Seq(...) and Map(...) often do end up having problems with type inference. It does suck to find which element is rogue. In fact any large type-inferred expression suffers this problem, it doesn’t even need to contain collections! But fixing that doesn’t need to be in scope for a collection literal syntax.

Using tuple syntax (...) for collections in fact makes it worse by ensuring that non-target-typed collections literals always end up with the wrong type (TupleN) with a totally different and unwanted set of methods operations, rather than a reasonable default (Seq) that will largely work for most things even if it might not have the exact asymptotic complexity you want

1 Like

Yeah, if only there was a proposal that allows specifying the element type explicitly somehow so that the compiler can point out the rogue element. Like, I dunno, #[Int](1, 2, "3") or something.

The library is now published on maven. Enjoy!

//> using dep io.github.dfiantworks::fromtuple::0.1.1
//> using option -language:implicitConversions

import fromtuple.convertAll
import collection.immutable.{ListMap, ListSet}
val l0:  List[Int]          = (1) //Scala sees it as a literal 1 and not a tuple, but this is supported
val l1:  List[Int]          = (1, 2)
val ll1: List[List[Int]]    = (l1, l1)
val ll2: List[List[Int]]    = ((1, 2), (3, 4))
val ll3: List[List[Long]]   = ((1, 2), (3, 4))
val ll4: List[List[Double]] = ((1, 2), (3, 4))
val l2:  Seq[Int]           = (1, 2)
val ll5: Set[Seq[Int]]      = (l2, l2)
val ll6: Seq[ListSet[Int]]  = ((1, 2), (3, 4))
val m1:  Map[String, Int]               = ("k1" -> 1, "k2" -> 2)
val m2:  ListMap[Int, String]           = (1 -> "v1", 2 -> "v2")
val ml1: Map[String, List[Int]]         = ("k1" -> (1, 2), "k2" -> (3, 4), "k3" -> l1)
val ml2: ListMap[Double, ListSet[Long]] = (1 -> (1, 2), 2.0 -> (3L, 4), 3 -> (1, 2L))
case class Foo[T](x: T, y: Int)
class Bar(val x: Int, val y: Int, val z: String)
val c1:  Foo[Int]           = (1, 2)
val c2:  Foo[String]        = ("1", 2)
val c3:  Bar                = (1, 2, "3")
val c4:  Foo[List[Int]]     = ((1, 2, 3), 4)
11 Likes

I thought about your original proposal with having a character replace the companion object, and it won’t work everywhere because of potential overloading.

case class Bar(arg: Int)
case class Baz(arg: String)
def foo(arg: Bar): Unit = ???
def foo(arg: Baz): Unit  = ???

foo(#(...))//error: this is ambiguous. 

That’s true, but there isn’t really a way to avoid that. Even Martin’s typeclass based proposal will be ambiguous if you overload between, say List and Vector.

I think I figured out how to make a strawman library for your proposal. No need for compiler changes.

2 Likes

That sounds interesting. I wouldn’t have thought it’s possible because of the special scoping rules for placeholders (similar to _), but I’m certainly interested to see what you can come up with. Many thanks for the work you’re putting into this!

// edit:
Having thought about it for a moment, perhaps this can be implemented by making use of the existing logic for _ placeholders… I. e. instead of val x: Seq[Int] = #(1,2,3) you write something like val x: Seq[Int] = magic(_(1,2,3)), and the magic macro rewrites that to something like val x: Seq[Int] = Seq(1,2,3)

I’m really not convinced

I think the begginer-friendly thing to do is to spell things out: Seq(a, b, c), Map(a -> a1, b -> b1, c -> c1)
(I think this is even the case for languages like python which have [] but not a strong type system)

And furthermore, we already have brackets in the language.
It’s obvious for us that they play vastly different roles when after an identifier vs not, but for new users (whether experienced in CS or not) I think this would be very confusing

See for example

While teaching Scala to my partner (never programmed before), she declared methods in ways I was surprised were valid like:

def name   (param1:Int)  (param2 :String) :Char=
  ???

This showed me how unfamiliar beginners are with white-space, and therefore the following seems very plausible:

def test [a, b] (c: Int, d: Int)

test [1, 2] (3, 4) // method test does not take more parameters
// What's going on ? It matches the signature exactly !
4 Likes

This is fair for you to think so, but the entire developer community seems to disagree. Nobody writes list([a, b, c]) or map({a: a1, b: b1, c: c1}) in Python even though it is perfectly valid, because the list() or map() wrapper adds no value at all and just adds verbosity to obfuscate things.

Let me repeat the survey I posted earlier of programming language syntaxes

  • [...]: Javascript, Python, PHP, C#, Typescript, Ruby, Swift, Kotlin, Objective-C, Rust, Dart
  • {...}: Java, C++, C (in limited scenarios)
  • [...]int{...}: Go
  • c(...): R
  • @[...]: Objective-C
  • @(...): Powershell
  • Seq(...): Scala

Basically the whole programming language world has standardized on having some anonymous collection literal syntax. More than half of it has standardized on the [a, b, c] syntax, and the others have tried to approach as concise an alternative as possible such as c(a, b, c) or @[a, b, c]. Others like Kotlin aren’t there yet, but are openly discussing such a move (https://youtrack.jetbrains.com/issue/KT-43871)

It’s entirely possible that the Scala language is right here that the verbosity of collection factories is the best way to teach beginners, and the whole rest of the world is wrong in wanting a concise anonymous collection literal syntax. But I would bet that the rest of the programming language world knows more about “what makes things beginner friendly” than the famously-not-beginner-friendly Scala language, and in this kind of basic syntax we should strive to learn from them. That doesn’t mean we should blindly copy whatever syntax or language features they have, but at least we should recognize that Scala is not the paragon of beginner friendly programming language features, and see how we can adapt the best parts of other languages such as the [a, b, c] collection syntax to fit into Scala in the best possible way

4 Likes