its would be very simple to implement in comparison to other proposals, as it would reuse existing core features at desugar phase.
Sure, but easy to do is not a good argument for adding a feature.
It would add syntactic overload just because âother languages also have specific syntax for thisâ. One of the strengths of Scala is that it has a lot of features that do not require these bespoke solutions for every little thing.
Both the prototype by @JD557 (scastie) and my earlier experiment that uses a macro to delegate to the companion of the inferred type (scastie) allow more or less the proposed language extension, except using method call syntax on some imported name instead of overloading brackets, i.e., some variant of val x: List[Int] = *(1, 2, 3)
.
Maybe there are some inconvenient edge cases with those implementations, but it seems much more fruitful to explore those implementations over syntactic additions to the compiler.
Some other notes:
⢠The âlookup a method in the companion object of the inferred typeâ prototype is ~30 lines of code, not sure how that does not count as simple to implement.
⢠Is it even feasible to add type class instances with the frozen standard library?
⢠The only need that was expressed by people for this feature was to make certain patterns in external libraries more convenient, was there any requests to make Seq(1, 2, 3)
and Map("a" -> "b")
more convenient?
⢠My impression is that the need was specifically for cases with many constructors, where it is uninteresting to remember which name is required at what position in the structure, because the structure is known to the compiler anyway. Allowing this usecase would require explicit adoption by third party libraries.
⢠Yet, how long until third party libraries could realistically make use of this? Given that it is such a minor syntactic convenience, and that libraries are encouraged to stay on old Scala versions, it seems this might take some years. I think itâs likely that people would just keep using the established patterns indefinitely.
I canât follow this reasoning at all, in fact I think the opposite is true: the â#
as placeholder for companion object of expected typeâ re-uses existing machinery to a much greater degree than the typeclass based proposal.
- like the typeclass based proposal it re-uses the compilerâs existing notion of an expected type (we know the compiler has such a notion because it can infer argument types for lambda expressions)
- unlike the typeclass based proposal, it re-uses the existing scoping rules for lambda expressions with
_
placeholders - unlike the typeclass based proposal, it enables re-use of a vast amount of functionality in and around the companion object, like the
apply
method, but also things like theof
method onLocalDateTime
or collection conversions in expressions likemyList.to(#)
where the expected type is some other collection type
And that is actually the main reason why I think this design is much superior: it actually fits with the language as it exists today. The typeclass based proposal is a Swift feature bolted onto Scala to make it look more like Python.
And that is another reason why the âcompanion object placeholderâ idea is a better design: the moment itâs released, you can start using it. Your libraries donât need to change. You donât need to add typeclass instances to your code. It just works.
Thatâs indeed really not a good argument to introduce some odd and extremely irregular syntax.
âBecause the mainstream does itâ was never an argument in Scala. Actually the opposite. Scala dismissed a lot of mainstream stuff to create in the end superior and groundbreaking solutions, leading the way to substantial improvements of the status quo in the whole programming language space! We should not look back at the others, we should look forward to still be the one who gets copied, not the other way around.
I like the idea.
I started to use Scala much more for âscriptingâ since Scala-CLI, and I love it. But in that domain such shortcuts as proposed would be really welcome. For a single Map
or Seq
it makes of course no difference. But if you write code like in a primitive dynamic language (which you do often when you write âscriptsâ) you start to have a lot of nested Maps and Lists. Than even the three letters start to matter, as theyâre just clutter. I would prefer a shorter, visually more lightweight syntax for that use-case. But I take the lib solution anytime before some odd syntactic overload!
If nothing happens here Iâm going to steal the code that was proposed so far⌠I like it. Iâm very grateful to @JD557 for providing it!
Maybe someone wants to publish a mico-library should this here not move, so no code needs to be stolen?
Iâm not sure about the âkatanaâ, though. I think @odersky is right in that that seems a little bit âtoo powerfulâ. JD557âs solution also works with type-classes, which is imho the right approach as it limits the shorthand syntax to only where TC instances are provided. That seems about right on the power level. Itâs flexible, but not too magic. (Granted, a few years back TCs were considered advanced, and sometimes âtoo magicâ. But weâre over this thankfully!)
There is no way to consistently argue that the _
placeholder in lambda expressions is OK but the #
placeholder for the companion object is not. Itâs completely arbitrary, and probably based on the fact that people have had years to get used to _
whereas #
is a new idea.
I donât think you can reasonably nest underscores to arbitrary levels, where each instance may have completely different meaning. And if someone tried to actually write something like that it would very likely not pass code review anyway.
I would agree with you if it was only one level deep (like the underscore usually). But the construct allowedâand actually encouragedâto write for example #(#(#(foo, bar)))
, where each #
could be something completely arbitrary depending on where this is written.
If you moved that construct elsewhere arbitrary things could happen. Maybe it even compiled, but did something unexpectedâŚ
So I donât think the analogy really holds.
Shorthands are nice, but too much and too powerful shorthand syntax results in so called âwrite only codeâ. Usually you donât want to encourage people to create âwrite only codeâ. Nobody wants to read âPerl one-linersâ ever again! But an especially simple and short symbolic syntax would do exactly this. Thatâs Martinâs argument, and I think itâs reasonable.
OTOH, if I had *(*(*(foo, bar)))
I actually know what this is. Without much context. The context would only tell me which kind of (nested) Sequence this is. But it couldnât be something completely different. Thatâs a big difference.
The Scala language absolutely allows nesting underscores as deep as you like, it doesnât impose any restrictions on that.
val f: (((((((() => Unit) => Unit) => Unit)
=> Unit) => Unit) => Unit) => Unit) => Unit =
_(_(_(_())))
Is that reasonable code? Of course not, but we donât impose arbitrary restrictions on underscores to prevent it. Instead, we trust programmers to not use the feature in this way despite the fact that they absolutely could do so. So yes, the analogy absolutely holds, and practical experience shows that people do know how to handle such features.
This whole idea of âyou can nest this stuff, which makes for unreadable code, hence we canât allow itâ is just complete hogwash. We can nest loops, for comprehensions, objects, classes, traits, if expressions, matches, lambdas and probably a thousand other things, and when somebody says âbut it hurts when I do thatâ, the answer is always the same: âdonât do that thenâ. This case is no different. Not one bit.
If you like bracket soup so much, where everything is context dependent, have you considered to move to the Red language?
Also could you provide a realistic example of such nested underscores, like all the nested, realistic examples (which people would actually start to write!) of the use of the proposed feature?
Iâm still not convinced the analogy holds.
The whole point of the example is that itâs not realistic, and that we still didnât place arbitrary restrictions on _
, despite the fact that it makes such nonsense possible. That didnât turn out to be a problem, and thereâs no reason to think that #
would be any different.
It probably would be used and nested more than _
, because deeply nested data structures occur more frequently than deeply nested lambda abstractions. Thereâs a word for features that are used a lot. Theyâre called useful.
On a more constructive note: @mberndt could your proposal be implemented as a compiler plugin? As a prototype, to actually play with the feature?
I mean, I see some value in some âdata literalâ syntax. There could be maybe use-cases which are fine and safe. But Iâm honestly not sure whether it would result in maintainable code.
One needs to take things like refactorings under consideration. But also that code isnât always written by the most reasonable peopleâŚ
Scala is in that regard nice as hasnât much âfoot gunsâ compared to most other languages. I think one reason for that is that it does not lighthearted adopt everything that looks ânice and convenientâ (at first). Itâs always a balance between safety and power. Think C++: Itâs very powerful, and it has all the features, and lets you do whatever you want, however you want. But itâs very easy to shoot yourself in the foot with that language. Some people might be able to handle all that, but frankly most canât. I would not like to see Scala introduce potential foot guns, even if these were only foot guns for less experienced people.
But like said, maybe itâs all fine, and this would be a nice addition. So how about creating a compiler plugin? Whatâs actually needed from the compiler to implement this?
I am probably vastly oversimplifying things here but here it goes.
Not sure if anyone mentioned approaches like this in this thread, I apologize if I am repeating somethingâŚ
Sequences
Could we (ab)use tuples for this?
Automatically convert a tuple of correct type to a List for example:
val list: List[String] = (1, 2, 3)
Maps
Essentially a list of Tuple2sâŚ
val map: Map[Int, Int] = (
1 -> 1,
2 -> 2,
3 -> 3
)
Case classes
Now named tuples are a logical choice:
case class Point(x: Int, y: Int)
val point: Point = (x = 1, y = 2)
We can go from a case class to named tuple, why not other way around tooâŚ
This approach reuses existing syntax, and it is mostly easy to grasp, since the concepts are similar.
It reminds me a bit of haskell syntax.
I was playing around with this and implicit conversions, but I donât think this can currently be done without some changes to the compiler:
Type inference issues aside (maybe someone smarter than me can fix some of those), thereâs always going to be the Tuple1
elephant in the room.
I have actually had this idea before, I just havenât expressed it in this thread. But Martin shot down both the original []
proposal as well as the #
idea, and assuming he doesnât change his mind, there is room for a separate proposal to re-use named type literal syntax for case classes. Iâve been wanting to create a separate thread for this for some time now, I just didnât get around to itâŚ
Iâm pretty sure it can be done with macros. Iâve done similar things.
After trying it out, the only thing that prevents me from implementing it is the problem of covariance within implicit conversions
Cool update!!!
I created a strawman fromtuple
library that utilizes implicit conversions. Indeed due to bug inline implicit conversion infers `Nothing` for covariant types ¡ Issue #19388 ¡ scala/scala3 ¡ GitHub the conversion cannot be applied directly on the collection types, but I created an opaque ~[T]
that can be used as a wrapper for the target type to trigger the implicit conversion and force an invariant conversion.
The library converts the following composition of tuples to:
- List/Set/Seq/ListSet/Map/ListMap
- Map/ListMap require
(key1 -> value2, key -> value2, ...)
tuple patterns - New class instances by using the default class constructor.
Int
toLong
andDouble
weak conformance- Types that do not match the above patterns first try summoning
Conversion
before giving up. - Type mismatch errors (or
Conversion
implicit custom errors) are positioned to the specific arguments that are at fault. This is a much better user error handling experience than manually collection composition because of this!
Here are a few examples:
import fromtuple.*
import collection.immutable.{ListMap, ListSet}
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)
Compiler error example:
import fromtuple.*
val x: ~[Map[Double, Set[Long]]] = (1 -> (1, "2"), 2.0 -> (3L, 4), "3" -> (1, 2L), 4)
-- Error: Spec.test.scala:2:45 -----------------------------------------------------------------------------------------------------------------------2
|val x: ~[Map[Double, Set[Long]]] = (1 -> (1, "2"), 2.0 -> (3L, 4), "3" -> (1, 2L), 4)
| ^^^
| Found: ("2" : java.lang.String)
| Required: scala.Long
-- Error: Spec.test.scala:2:67 -----------------------------------------------------------------------------------------------------------------------2
|val x: ~[Map[Double, Set[Long]]] = (1 -> (1, "2"), 2.0 -> (3L, 4), "3" -> (1, 2L), 4)
| ^^^
| Found: ("3" : java.lang.String)
| Required: scala.Double
-- Error: Spec.test.scala:2:83 -----------------------------------------------------------------------------------------------------------------------2
|val x: ~[Map[Double, Set[Long]]] = (1 -> (1, "2"), 2.0 -> (3L, 4), "3" -> (1, 2L), 4)
| ^
| Invalid `key -> value` pattern for Map
3 errors found
If we fix the Scala bug above then we can change the library so that there is no need to use ~[T]
.
The idea of implicit tuple conversions is just bad bad because it doesnât handle lists with one item in a sensible way.
@soronpo one issue with the tuple-syntax approach is the ambiguity when it comes to 1-element collections: is val x = (1)
an Int
or a Seq[Int]
?
The conversion requires explicit type ascription. What is the ambiguity? val x = (1)
triggers no conversion.
There is no ambiguity, (42)
is unambiguously an Int
, which makes it impossible to express List(42)
using that syntax.
Add to this the problem that this provides minimal utility because itâs limited to collections, and the problem that it can be confused with actual tuples, and you get a feature that I certainly wouldnât use, nor would I recommend anybody else to use it. At least Martinâs proposal canât be confused with tuplesâŚ