For example, Python has lists, sets and dictionaries as language-level primitives that can be constructed using syntactic sugar [1,2,3], {'a': 1, 'b': 2}
while Java has interfaces for each of these structures, no main/canonical/recommended implementation for them, and no special syntax for creating or accessing them. Why would a language avoid raising a data structure to first-class status by giving it such a syntax?
-
1$\begingroup$ I think "intrinsic" is a better word than "primitive" here. Normally a primitive type means one which represents a single value, as opposed to a compound type or aggregate type which represents a grouping or collection of values. $\endgroup$– kaya3Commented Jun 28, 2023 at 14:38
-
$\begingroup$ Intrinsic has a very heavy meaning in compilers and optimization though... builtins might be an even better nomenclature? $\endgroup$– kouta-kunCommented Jun 28, 2023 at 14:48
-
2$\begingroup$ Wikipedia seems to think "intrinsic function" and "built-in function" are synonyms, but if there's a technical difference then perhaps built-in is better, sure. $\endgroup$– kaya3Commented Jun 28, 2023 at 14:55
2 Answers
Why would a language prefer to mark its' builtin primitives as first-class citizens with specific syntax for them?
Why have special syntax for common data structures? Because it’s convenient! No more and no less. But this is not fundamentally tethered to whether the data structure implementations are wired into the language. Your original question asked “What are the downsides of data structures being interfaces instead of primitives?” and while your revised question focuses on that somewhat less, I think an exploration is still relevant.
Whether data structures are somehow primitive is essentially orthogonal to both (a) whether they are accessed through interfaces or (b) whether they support literal syntax. After all, what makes something “primitive”, really? There are many different degrees of “primitiveness”:
At the deepest level, a primitive operation might be so deeply wired into the evaluator that it is actually impossible to (acceptably) implement as a derived concept. For example, we would generally call conditional branching or lambda expressions primitive in this sense.
Slightly less deeply, we might call something primitive if it has special integration with other features of the language. For example, the JavaScript
typeof
operator distinguishes certain wired-in classes of values, so reimplementations will be distinguishable from the builtin versions.We might call other things primitive simply because they have special syntax, even though they are really otherwise just ordinary values in the programming language. Literal expressions fall into this category.
Finally, we might call certain things primitives if they’re just such a deeply integral part of the standard library that it is impossible to choose not to include them in your program because certain evaluation rules or aspects of the compiler rely upon them. Many classes in the
java.lang
package fall into this category, likeNullPointerException
(though some of them are primitive in a somewhat deeper sense).
None of this really has anything to do with how you choose to expose the functionality itself. Interfaces in the Java sense are really a combination of two features:
They’re a feature of Java’s static type system that allows for a form of polymorphism.
They’re a feature of Java’s dynamic type system that allows checked runtime casts and reflection.
The former of these simply doesn’t apply to Python because Python is dynamically typed, and the latter doesn’t apply to Python because Python doesn’t have checked runtime casts or interface-based reflection. So the answer to “why doesn’t Python expose lists via a Java-style interface” is “Python doesn’t have Java-style interfaces”.
Now, of course, it is true that Python chooses to specify some of the implementation details of its lists, but there is no fundamental need for it to do so. The concrete implementation of a Python list as a linked list or an array is only observable in its performance characteristics. The implementation of lists could be altered, and Python programs would still work, though they might perform differently. So you really need to pin down what you mean by “interface” in order for the question to mean much.
Finally, there is not any fundamental need for literal syntax to correspond to a builtin or even any single concrete type. In Haskell, numeric literals are overloaded, so the literal 42
can be given a user-defined type; the meaning of the literal is determined based on its type. Furthermore, the OverloadedStrings
and OverloadedLists
language extensions generalize this concept to string literals and list literals, respectively. However, this overloading adds some complexity to the language, and Python has always been a language where there should be “only one way to do it”. So the fact that Python does not choose this route is not particularly surprising.
tl;dr: Python chooses to have special syntax for built-in data structures because it’s convenient and because Python programmers have a culture of preferring simple, consistent defaults over more complex flexibility. Java chooses not to provide special syntax because it is exceptionally syntactically conservative (it famously does not permit operator overloading). But you could use or design a language that works differently if you have different preferences or priorities.
-
$\begingroup$ Sorry I changed the end of the question while you were writing, but this is a great answer and exactly what I was asking about! $\endgroup$ Commented Jun 28, 2023 at 6:05
-
2$\begingroup$ For another possibly interesting example of not wiring data structures into the language while still having syntactic sugar for them, C# collection initializers allow succinct initialisation of any collection, very loosely defined as anything that is
IEnumerable
and has anAdd
method (which is a very low bar to meet, and supports both single and multiple parameters in that method, or even multiple overloads). $\endgroup$– MiralCommented Jun 29, 2023 at 2:34
The benefits to syntactic sugar for primitives is obvious; it's much easier to construct common collections, and much more readable too.
Disadvantages are less obvious:
Avoiding being opinionated
Syntax must by nature pick one specific implementation for lists. This may be undesirable. For example in Java, because of its Object Oriented principles, emphasizes you must pick the interface separately from the implementation, so choosing an ArrayList
instead of a LinkedList
would violate this principle.
Running out of brackets
Most languages that have syntactic sugar use [] {} ()
maybe <>
. You can in some cases re-use the same syntax for multiple types of data structures like Python, which uses {}
for both sets and dicts; but even there, there is ambiguity.
This leads fundamentally to a divide between first degree and second degree data structures. This encourages programmers to reach for the basic list or dict when something like a heap or binary search tree would fit better.
Syntactic Ambiguities
There is a common ambiguous parsing case in Javascript:
()=>{
a: 2
b: 3
}
(note the missing comma is intentional)
You may expect this to return a dict with keys a
and b
but really this is a function body. To create an object you would need to wrap it in parentheses.
Just not having syntactic sugar for data collections, or restricting it to just []
in the style of C, would avoid this problem.
-
1$\begingroup$ "Syntax must by nature pick one specific implementation for list" – That's not necessarily true. There is no reason why a literal cannot be syntactic sugar for a method call. In fact, a lot of newcomers to Ruby seem to be surprised by the fact that
[]
,""
,{}
, and friends do not callArray.new
,String.new
, andHash.new
. I always like to look at stuff that surprises newcomers to try and get a sense of what is "natural", so literals translating into method calls seems to be a natural thing … just not to language designers. There was a research project called "scala-virtualized" … $\endgroup$ Commented Jun 28, 2023 at 23:44 -
1$\begingroup$ … whose goal it was to improve Scala's suitability for internal DSLs by translating most builtin language features (e.g.
while
loops, conditional expressions, and, relevant to this discussion, object construction) into method calls (i.e. virtual methods, hence the name) the same way it is already done forfor
comprehensions. The Ioke and Seph languages translate literals into method calls, e.g.23
is translated intointernal:createNumber("23")
(not quite: the string being passed to the method is not an Ioke string, it is a "strange string", $\endgroup$ Commented Jun 28, 2023 at 23:52 -
1$\begingroup$ … i.e. a native string of the underlying runtime platform (JVM, .NET, ECMAScript, etc.)). See ioke.org/wiki/index.php/Guide#Integers and ioke.org/dok/latest/kinds/DefaultBehavior/Internal.html#C0060 String literals are translated into
internal:createText
, Regexp literals intointernal:createRegexp
, and so on. Collection literals are just normal "around-fix" methods, i.e.[1, 2, 3]
is just am different way of writing[](1, 2, 3)
, and so on. $\endgroup$ Commented Jun 28, 2023 at 23:53