`obj[...]` syntax is "legacy" and should be removed

(cross posting my discord post) :fire:

I have said it a few times already and probably nothing will change but I will not give up yet so here we go:

  • obj[...] syntax is “legacy” and should be removed! :astonished: :thinking: :warning:

Please repeat with me :stuck_out_tongue:

  • obj[...] syntax is “legacy” and should be removed
  • obj[...] syntax is “legacy” and should be removed
  • obj[...] syntax is “legacy” and should be removed

Reasons

  • :woman_technologist: it is confusing for developers (collision with mojo function parameters)
  • :robot: it is confusing for AI (which will probably write most of the code in the future)
  • deviation from the official mojo function anatomy

The anatomy of a mojo function is

def hello[params](args)

which can be used as:

  • hello[3]("world")
  • hello("world")
  • hello()

:information_source: Note: [...]is always a parameter and (...) is always an argument and required to call a function!

We programmers we have many principles/best pratices and designs like “keep it simple”, “consistency” and more but then create

  • data[idx] :no_entry_sign: :cry:

which violates everything

  • [idx] argument used where parameters should be! :thumbsdown:
  • () actual function call missing :thumbsdown:
  • Additional side note: Terrible IDE support (same for most/all other langs). Not discoverable with the other functions (hitting . and exploring). No hover documentation. Not autocomplete… :thumbsdown:

At the moment is feels like a second, separate function world is created which IMO is very unfortunate and confusing:

  • data[123]
  • data[abc=5]
  • data[x=-1]

Suggestion

Use the defined anatomy of a mojo function call everywhere and consistently!

  • data[0]data.get(0)
    Don’t worry about the extra 4 letters. Instead we get in return
  • consistent mojo function syntax!!!
  • better IDE support (discoverability, docs, autocomplete, …)
  • AI will write most of the code anyways :slight_smile:

See the related topic:

I think using [] for indexing is pythonic and should be kept, like having + for addition instead of .add().

But I agree that having the same [] for type parameters might be confusing for newcomers to Mojo but this reflects the syntax of Python type parameters: PEP 695 – Type Parameter Syntax | peps.python.org

The root problem is Python choosing [] for generics even though it was already being used for indexing. I can see how that can be confusing, especially as Mojo parameters can take numbers rather than just types. makes it hard to tell whether obj[1] is an object taking a type parameter or being indexed.

I see this point. But in most cases it obvious whether 1 is an index or a type param:

# Indexing into a collection:
a = my_list[1] # an index

# All other usages:
b = my_func[1]() # a type param of a `def`
c = MyType[1]() # a type param of a `struct`
comptime OneType = MyType[1] # a type param of a `struct`

I think the not so obvious thing is that in Mojo an integer like 1 can be a type param too.

In many (most) cases, type params are not integers and then the distinction trivial.

I feel like Python should have rather made the generics using <…> instead of […]. <…> is not used anywhere in the language that I know about while […] is used in a lot of places. Then Mojo would have also used <…> instead of […] for parameters.

When I first saw Mojo (without looking too deeply at documentation) I wondered if the generics would stay within […] and parameters would be written as <…>. Or even the other way around to fit with Rust’s way of generics. This would probably look a bit absurd.

Instead of this:

def repeat[T: Writable, //, times: Int](msg: T) -> None:
    comptime for i in range(times):
        print(i, msg)

def main() -> None:
    repeat[10]("Hello World")

I expected this:

def repeat<times: Int>[T: Writable](msg: T) -> None:
    comptime for i in range(times):
        print(i, msg)

def main() -> None:
    repeat<10>("Hello World")

It does not look pretty, but even if Mojo rather used <…> for generics and parameters then I would have been absolutely fine with it.

I would however not want to type something like my_list.get(0) instead of my_list[0]. The bracket syntax is something I am very used to with lists and if I am being honest it was never that confusing for me to write it once I knew that was the syntax to use to index into a list or get a value from a dictionary.

1 Like

Alright, I was convinced that it can be valueable to keep the obj[...] indexing syntax for some use cases like

  • a[i][j] = b[j][I] instead of a.get(i).set(j, b.get(j).get(i))

However, I still think it’s valuable to restrict this to use cases that are 100% unambiguous like

  • list indexing mylist[0] :white_check_mark:
  • dict indexing mydict[key] :white_check_mark:

IMO its bad for any other use cases that are not 100% clear like:

  • s[0] :warning: is it clear what it returns?
  • s[bytes=1:4] :cross_mark:
  • s[codepoint=3] :cross_mark:
  • s[regex="\d+"] :cross_mark:

Those should be normal functions

  • s.get(bytes=1:4) :white_check_mark:
  • s.get(codepoint=3) :white_check_mark:
  • s.regex_extract(regex="\d+") :white_check_mark:

Reasons

  • creating 2 function worlds
    • obj.<...>: standard function world
      • this is the single place where everyone can discover functionality and is the go to!
      • we get auto complete + documentation
    • obj[...]: 2nd function world
      • please keep this as small and limited as possible
      • IDE/tooling/dev experience is terrible! (yes everything can be fixed but looking at the most used setup in the world which is python+vscode it is still terrible after many, many years!!)

+1 for C++ -style angle brackets <> for comptime arguments, instead of []

Mojo depends heavily on that mental distinction - it should be obvious and context free. “[]” is like wanting to be everything for everybody, ending up to be confusing for most

This goes back to Feedback request: Should Mojo adopt "specialization" for what we currently call "parameters"? - #17 by Nick
”<>” == comptime, “[]” == runtime (whatever you call it)

1 Like

Python recently (3.12) opted to use [] for type parameters and I see no strong reason to deviate from Python here.

Does Python make a distinction between comptime and runtime?

Mojo leans heavily on Python, but it also needs to differentiate itself. It should not replicate Python just because it is more familiar to Python programmers. Both C++ and Java use ‘<>’ syntax for decades, as you point out Python only recently introduced it (and I personally have never used it)

In the ‘fn’ versus ‘def’ case I agree that there is no strong reason to deviate - but in this case, IMHO there is one

Types in Python are just hints and therefore static and erased at runtime. But it too uses [] for indexing/keys as well as for type parameters. Maybe it is less confusing in Python as the type system is less powerful.

Types parameters were introduced in 2022. Thats recently from the viewpoint of a person as old as me. Your milage my vary. :wink:

In Python it is an add-on, literally an afterthought. For Mojo, this is its essence of being - make or break.
If types don’t matter, one would use Python

I would state it differently: If performance don’t matter, I would use Python. But true - Mojos comptime type system is a means to this end.

There are other reasons to have a type (hint) system in Python like supporting the LSP and making code easier to understand and reason about.

1 Like

In Python, types are cognitive aids aimed at humans. In Mojo, they are more like semantic guardrails aimed at the compiler, they express programmer intent and enforce invariant constraints.

The Evolution: From “Hints” to “Hard Constraints”

To visualize how these two approaches differ in practice, consider how each language handles a simple variable:

Feature Python (PEP 484) Mojo
Primary Audience Humans and IDEs The LLVM Compiler
Enforcement Optional (Runtime ignores them) Mandatory (Strictly enforced)
Memory Impact None (Objects remain boxed) Direct (Defines data layout/size)
Speed Documentation tool Performance driver
2 Likes

Where Mojo has compile-time, Python has, what can loosely be called, ‘load-time’ .
Essentially this is when the Python interpreter looks at a file for the first time and evaluates top-level statements, such as class declarations, and the signatures of methods associated with the class, to load the class and its methods into memory.

Distinction between runtime and ‘load-time’ in Python:
Python’s function signatures are basically top-level statements, and are evaluated at load-time.
Function bodies are executed at runtime - this happens after all of the relevant code has been loaded by the interpreter and the function is called.

(From what I understand) for Mojo to have similar functionality, Mojo has a parse-time interpreter which evaluates ‘comptime’ expression at parse-time, which is conceptually similar to Python’s ‘load-time’, in the sense that all of the information related to a function’s signature-definition is evaluated.

There are still some nuanced differences between comptime and ‘load-time’.

The use of the ‘comptime’ keyword improves on Python because the keyword clearly indicates when the expression is evaluated. This reduces confusion.

The use of […] for parameters, clearly indicates that the values in the brackets need to be compile-time-known. This reduces confusion.
And the use of […] comes from Python’s own syntax for generic containers.
E.g. a: List[Strig] = ["hello", "world"]

Similarity for the sake of familiarity.
For a dev to transition to a new language takes time, which costs money.
So the idea is to make the transition as easy as possible (where it makes sense).

With regards to prior established patterns, JavaScript and Typescript are very similar for the same reason.

It is more likely that I Python dev will transition to Mojo, than a Java/C++ dev to transition to Mojo, although they could.
Python remains the primary transition-usecase.

As a cheeky piece of bikeshedding, I’ll note that D just uses regular parentheses when declaring both template/generic parameters and runtime parameters, and uses !(...) and (...) at the call site to distinguish the two – and as a nice detail, one can just use ! at the call site if there is a single template parameter.

e.g.

// only one parameter list for a function, so they're treated as runtime parameters
int mul (int a, int b) {
    return a * b;
}

// two separate parameter lists for this function, first is compile-time/template
// params, second is runtime params
CommonType!(T, U) add (T, U) (T a, U b) if isNumeric!T && isNumeric!U {
    return a + b;
}

Note that [...] for generic/compile time parameters is fine at the declaration site. One could consider ![...] at the call site if one wanted to distinguish from indexing look-up.

Python programmers being the main audience, is an even stronger argument for ‘<>’. Because for that large group it becomes particularly important to make them aware about the differences between where they are coming from, and what they are getting themselves into.

According to Gemini, meta-programming in Python is a powerful but niche technique that is not widely used in day-to-day coding. Most Python developers rarely need to use custom meta-programming features, and it is largely considered “deeper magic” by community experts

C++ and Java programmers are already familiar with <> for templates, so no issue there. The goal shouldn’t be to keep syntax as similar to Python as possible, the syntax should help build a mental model of what’s going on.

Comptime meta-programming as a concept is relatively new to Python, but standard in Java and C++. IMHO it shouldn’t be modeled after the new kid on the block, where it is rarely used by most programmers. This is an opportunity to build a bridge.

I’m not really sure what you mean by metaprogramming in Python, but doing things with introspection and type specialization, custom decorators, and so forth never seemed that difficult? They may not crop up that much in typical Python use-cases but often when people consider stuff to be “deeper magic” what it really means is that guides and examples of how to do it aren’t readily available.

Much more important than square versus angle brackets, IMO, is a factor that IMO is a serious problem with C++ and still somewhat of an issue with Rust: that metaprogramming often winds up looking like a completely different programming language. But it doesn’t have to be that way.

One of the reasons I keep talking up D in this forum is because it’s a great example of metaprogramming made easy. Most of the constructs you have to use look and feel exactly like their runtime equivalents. Folks who’d never have metaprogrammed in C++ wind up doing it as a matter of course in D because they can largely just apply their existing experience.

Take this Mojo stdlib code as an example:

comptime ConditionalType[
    Trait: type_of(AnyType),
    //,
    *,
    If: Bool,
    Then: Trait,
    Else: Trait,
] = Variadic.concat_types[
    Variadic.filter_types[
        *Variadic.types[T=Trait, Then],
        predicate=_ConditionalTypePredicate[Trait, If, ...],
    ],
    Variadic.types[Else],
][
    0
]

Obviously folks are working with the features that exist right now, but … this doesn’t look like runtime Mojo code. But what if the syntax allowed for, say:

comptime ConditionalType[
    Trait: type_of(AnyType),
    //,
    *,
    Condition: Bool,
    A: Trait,
    B: Trait,
] = A comptime if Condition else B

(… which it may well in the future, I’m sure Mojo’s designers would like it to be this simple). Would Pythonists find that so hard to reason about?

1 Like

[] makes more sense for generic type parameters, in my opinion, given that there is already established official syntax for generics in Python, as of Python 3.12, as per the following code:

def get_max[T](a: T, b: T) -> T:
    return a if a > b else b

print(get_max(10, 20)) # T is inferred as int
print(get_max("apple", "banana")) # T is inferred as str

Note that following Python 3.6 and 3.9, the use of typed Python has increased substantially.
Refer to one of the newer Python web-frameworks, FastAPI:
" building APIs with Python based on standard Python type hints. "

The guiding principal should be to keep the syntax as similar to Python as possible, where it makes sense. Given that Python has established syntax for generics, it makes sense, IMO.

I would argue that the use of generics/metaprogramming is more prevalent in Python compared to languages like C++/Java, the difference being that its use in Python is less obvious because of Python’s minimalist syntax. For example, the above Python code can be rewritten in this very common form:

def get_max(a, b)
    return a if a > b else b

print(get_max(10, 20)) # T is inferred as int
print(get_max("apple", "banana")) # T is inferred as str

This common use of generics, and ease of metaprogramming in Python, is why, IMO, Mojo has such great support for generic/metaprogramming.

Python has never needed a concept of comptime, because everything is interpreted, meaning everything is ultimately a runtime value. Python allows for certain code to be evaluated when the interpreter loads a file, which is like comptime

Python has had the ability to do these complex things (metaprogramming/something like comptime) for a very long time - it just did not have, or did not need, syntax or a name for these things, because these features are so well-integrated into the language.

Interestingly, Python predates Java.

1 Like

def get_max(a, b)
return a if a > b else b

print(get_max(10, 20)) # T is inferred as int
print(get_max(“apple”, “banana”)) # T is inferred as str

Your example perfectly illustrates how this is the compiler inferring meta-programming aspects, not the programmer consciously specifying them. Python makes it easy to gloss over the very details that make a difference in high performance code, because it doesn’t matter for typical Python programmer purposes.

Interestingly, Python predates Java

I was specifically referring to the meta-programming features that we are talking about, which were introduced in Python (long) after Java or C++

We are saying the same things, just using different syntax: [docs][Stdlib] Add draft proposal: Comptime parameterized return type syntax by jbemmel · Pull Request #6178 · modular/modular · GitHub