modularml / mojo

The Mojo Programming Language
https://docs.modular.com/mojo/manual/
Other
23.11k stars 2.59k forks source link

[RFC] Rename the `alias` keyword #171

Closed augb closed 1 year ago

augb commented 1 year ago

Request

Rename keyword alias to comptime.

Motivation

alias: named parameter expressions It is very common to want to name compile-time values. Whereas var defines a runtime value, and let defines a runtime constant, we need a way to define a compile-time temporary value. For this, Mojo uses an alias declaration. ...

from the Mojo docs

In the above definition, it is clear that an alias is really a comptime expression as opposed to strictly just another name for an expression. While it is likely there is good reason for using the keyword alias, I would suggest that this will lead to confusion, especially among newbies.

By using a keyword of comptime, the intention is clearly understood, whereas, the keyword alias is, while possibly technically "correct", not as clear. Aliases are typically used in other programming languages as a means of referring to a named entity (often modules) by another name. Many of these uses are in languages without the concept of compile-time expressions. In Python, for example, module aliases are created using the as keyword.

Description and Requirements

The term "alias" already has a history of usage that the Mojo usage may be inadvertently going against the grain on. Using a keyword such as comptime, has the benefits of clearly indicating the purpose of the expression and avoiding confusion that the current keyword alias may engender.

ksandvik commented 1 year ago

What if it's just a decorator, for example the alias dtype struct could have just a decorator on top rather than multiple keywords, or same with other expressions, or an expression inside the struct. As for the name, I don't really care.

nmsmith commented 1 year ago

Another option would be to use a succinct sigil, e.g. !let and !var, to denote compile-time variables. You could use the same syntax for compile-time control flow, e.g. !if. This would give us a consistent syntax for all compile-time code, allowing users to quickly identify the parts of a function that run at compile-time.

Currently, compile-time if requires a @parameter decorator, which feels a bit clunky to me, and it isn't able to decorate Python's ternary if-expressions. In contrast, a sigil is able to annotate any keyword or expression, no matter the context in which it appears. Thus, sigils are perhaps future-proof in a way that decorators are not.

Also, I think @parameter is inconsistent with how Python decorators work? As I understand it, decorators are defined to execute at runtime, and they act as wrappers around function objects, whereas @parameter does something very different.

lattner commented 1 year ago

comptime is really obvious to Zig folk, but that's not really our audience. You're right that alias may not be the right word to use here either. Aligning this around "parameter" could be a good way to go, but I'm curious if there are other suggestions.

Once nice thing about "alias" is that it is more obvious for the trivial cases like alias my_magic = 12312 or alias Int8 = SIMD[DType.si8, 1]. That doesn't make it the right thing, but it is a nice thing.

augb commented 1 year ago

comptime is really obvious to Zig folk, but that's not really our audience. You're right that alias may not be the right word to use here either. Aligning this around "parameter" could be a good way to go, but I'm curious if there are other suggestions.

I think my point is not to copy Zig; rather, it is to make clear that the value is a compile-time value. I'm open to other suggestions, but I think it might be better to err on the side of clarity.

Once nice thing about "alias" is that it is more obvious for the trivial cases like alias my_magic = 12312 or alias Int8 = SIMD[DType.si8, 1]. That doesn't make it the right thing, but it is a nice thing.

alias suggests that it could be at runtime or compile-time (as it is currently used). While I agree that your examples make perfect sense, these are still compile-time expressions, if I am understanding the docs correctly. The keyword alias gives no indication of this, neither does aligning around "parameter".

I could see the possibility of a future need for the keyword alias that can be used in a runtime-only context. Current use excludes that possibility.

Aligning this around "parameter" could be a good way to go, but I'm curious if there are other suggestions.

@lattner, could you elaborate on what you mean by this?

Updated: grammar

lattner commented 1 year ago

+1, I wasn't arguing for alias, just explaining the though process.

Aligning this around "parameter" could be a good way to go, but I'm curious if there are other suggestions.

I'm just saying that if we replaced the keyword "alias x = 42" with "parameter x = 42", then we can say "it's a declaration of a parameter" and that "parameters are all compile time expressions."

nmsmith commented 1 year ago

No love for the !let (or any other sigil) idea I mused about above? 🥲

lattner commented 1 year ago

No love for the !let (or any other sigil) idea I mused about above? 🥲

Not really, alias (regardless of what it is called) is a declaration of a thing. We need spoken vocabulary for programmers to describe these things. It isn't just about encoding things in source code for the compiler, it is allowing humans to communicate ideas as well.

Also, "let" values are not aliases. They've very different. A let isn't mutable after it is initialized, which is a flow sensitive property, e.g. this is allowed:

  let x : Int 
  if cond:
    x = foo()
  else:
    x = bar()
  use(x)

which isn't allowed for aliases.

nmsmith commented 1 year ago

Chris, I actually 100% agree with you that verbalizability is important. I'm a huge fan of English keywords.

Allow me to direct this conversation towards a core concern that I see with Mojo's metaprogramming. The core concern has nothing to do with keywords, it has to do with the model of compile-time execution that Mojo presents to users.

Although I was superficially proposing a sigil, my underlying intention was to propose a syntax that leads to a simpler model of compile-time execution. The code sample you gave above is a good starting point. You say that aliases can't be initialized in the same way that a let binding can. Why not? Having a single semantics for variable binding—irrespective of whether the binding occurs at compile-time or run-time—would reduce the number of concepts that people need to learn to do metaprogramming. Additionally, it leaves open the possibility of (eventually) making Mojo's metaprogramming language as powerful as its base language.

Possible syntaxes for compile-time execution

If Mojo is to offer run-time constructs such as let and if at compile-time, the syntax should reflect this.

Let's imagine that let can be used at compile-time. You'll need a way to request the compile-time version. A sigil such as ! or ~ (or whatever) could be used for this:

  ~let x : Int 
  ~if cond:
    x = foo()
  ~else:
    x = bar()

This was the proposal I was attempting to make in my original post. The only reason I was proposing a sigil is that keywords seem harder to read:

  comp let x : Int 
  comp if cond:
    x = foo()
  comp else:    // Putting the keyword here ensures that "if" and "else" remain aligned.
    x = bar()

But as I stare at this longer, I'm thinking that maybe the keyword is just as readable as a sigil. Regardless, the main idea is to have fewer concepts to learn. In the above examples, there's no alias, there's just let. The compile-time language is the same as the run-time language.

Let's ponder a few more syntaxes that are compatible with this model of execution.

Instead of putting sigils and keywords next to every statement, maybe chunks of code that are purely compile-time could be placed in their own block:

fn foo[flag: boolean]():
    compile:
        let x: int
        if flag:
            x = foo()
        else:
            x = bar()
    run:
        ...

And if a compile block has only one line of code, you could collapse it back into a keyword:

compile let x = 2

This would be the equivalent of an alias declaration.

The examples above can be situated in compile blocks because they don't mix compile-time and run-time code. But the following example from the Mojo docs does mix compile-time and run-time:

struct SIMD[type: DType, size: Int]:
    ...
    fn reduce_add(self) -> SIMD[type, 1]:
        @parameter
        if size == 1:
            return self[0]
        elif size == 2:
            return self[0] + self[1]

To represent this using the compile/run syntax, you'd need to interleave compile and run:

struct SIMD[type: DType, size: Int]:
    ...
    fn reduce_add(self) -> SIMD[type, 1]:
        compile:
            if size == 1:
                run: return self[0]
            elif size == 2:
                run: return self[0] + self[1]

This syntax—wherein one uses run to "escape" back to the run-time context—is reminiscent of string interpolation, and template languages like JSX.

Or maybe—going back to the earlier syntax—it is cleaner to prefix each line that runs at compile-time with the compile keyword:

struct SIMD[type: DType, size: Int]:
    ...
    fn reduce_add(self) -> SIMD[type, 1]:
        compile if size == 1:
            return self[0]
        compile elif size == 2:
            return self[0] + self[1]

Here, each line that doesn't begin with the compile keyword executes at run-time. There is no need to "escape" out of the compile-time context using the run keyword. I suspect this syntax would be easier to understand.

Focusing on the model of execution

To reiterate: my core concern is not about renaming the alias keyword, it is about the model of compile-time execution that Mojo presents to users. My assertion is that it would be conceptually simpler to offer a single set of constructs that can be used at both compile-time and run-time. Users wouldn't need to learn any additional concepts (like alias) to do metaprogramming. They just need a way to specify the time of execution.

This model might not end up being practical. I suppose it depends on what kind of metaprogramming system the Mojo devs are interested in building. But crucially: it would be possible to make gradual progress towards this model. The starting point would be to move Mojo's existing metaprogramming capabilities into this model, i.e. to replace alias with compile let, and @parameter if with compile if.

Feel free to bikeshed about the compile keyword, of course. (Call it comptime if you like.) In fact, maybe compile let x = 2 can even be abbreviated to compile x = 2. In that case, the metaprogramming model I've proposed would be entirely compatible with the rest of the discussion happening in this thread. We can bikeshed on the alias keyword, with the knowledge that it is a shorthand for a compile-time let binding.

At the very least, I believe this model of compile-time execution is worth seriously considering. The ultimate goal is simplicity — compile-time execution works the exact same way as run-time execution, except it also has the ability to "interpolate" run-time code.

Addendum 1

For compile-time execution to support all of the capabilities of run-time execution, it must be possible to invoke arbitrary functions. These functions would need to obey certain restrictions, such as not being able to do I/O. This doesn't require any support from Mojo's type system: if an I/O call is made at compile time, it would be sufficient to just abort compilation.

But this is a "stretch goal". It would be perfectly reasonable to disallow function calls at compile-time, and only allow basic operations such as arithmetic. It's a simple restriction that is easy to understand, and it leaves open the possibility of supporting function calls in the future.

Addendum 2

That said, certain kinds of I/O are extremely useful at compile-time. For example, the Rust compiler allows external files to be loaded into a string or byte array. One could imagine using this capability to parse a JSON file at compile time and load its data into a Mojo struct.

dom96 commented 1 year ago

I don't have any strong feelings, but just wanted to mention how another language does it. Nim uses const for this. Developers quickly learn that const means "compile-time", the same will probably happen with alias but if you want to reuse something another language already uses then const might be a good way to go.

That said, re-reading the docs for alias I get the feeling that it actually is strictly an alias and thus the alias keyword makes sense for it. Does the compiler forcefully evaluate expressions in an alias at compile-time? i.e. will alias x = 5 + aVariable; print(x) fail at compile-time because aVariable is not known at compile-time or is this simple expression replacement and x is just filled with 5+aVariable in the print? In Nim const x = 5 + aVariable; echo(x) doesn't compile.

augb commented 1 year ago

I'm just saying that if we replaced the keyword "alias x = 42" with "parameter x = 42", then we can say "it's a declaration of a parameter" and that "parameters are all compile time expressions."

@lattner, parameter gets closer, but I still think it is too overloaded in programming circles. After all, parameters are in the call signatures of a vast number of function definitions.

alias: named parameter expressions

From the docs, "named parameter expressions" are really "named (compiler) parameter expressions", aren't they? By using parameter for a narrow meaning such as this, I think we are adding to the cognitive burden of a developer.

... Nim uses const for this. Developers quickly learn that const means "compile-time", the same will probably happen with alias but if you want to reuse something another language already uses then const might be a good way to go.

@dom96 I like the idea of using const. It aligns with the concept of an unchanging value. C#, Rust, and many others already have the const keyword. Having this determined at compile-time doesn't throw any significant hurdles in the way, as far as I am concerned.

If const is problematic for reasons that are not immediately apparent to me, I would argue for compexp (instead of comptime). compexp = "compiler expression". I think this pretty clearly identifies what follows.

Does the compiler forcefully evaluate expressions in an alias at compile-time?

@dom96, my understanding is alias is always about compile-time named parameter expressions.

augb commented 1 year ago

Added a discussion that may have relevance to this issue here: #177.

Janrupf commented 1 year ago

It might even make sense to be able to just mark expressions and bindings as compiletime. In this case a keyword may be the better choice. With this it may look like the following:

comptime SOMETHING = 1234

if comptime(some_fn()):
    do_abc()
else:
    do_xyz()

some_call(SOMETHING, comptime(do_it()), get_user_input())

Reasoning here would be, that it is more flexible and allows partially marking things as compiletime. This however assumes that a reasonable optimizer is in place, which can detect a compiletime static condition and perform dead code elimination accordingly.

In this case it becomes questionable whether it makes sense to mark functions as compiletime (or even provide that ability). The call-site would control whether the called code is evaluated at compiletime, and if so, everything down the call tree is. At definition-site (as in, the fn declaration) it may then only make sense to mark it as compiletime if it should be required to be called in a compiletime context.

Regarding the naming, I think something like const or comptime would be a good idea, since it has a very clear intention. I like comptime the most out of all the proposed names so far, since it very clearly states what it does.

nmsmith commented 1 year ago

I updated my post above with a more detailed proposal for Mojo's metaprogramming syntax. Please check it out folks 🙏. In short: I think it's worth developing a clear, and easy-to-understand model for compile-time execution. Once this is done, it will be easier to figure out a good name for specific constructs such as alias.

By focusing too narrowly on the name for the alias construct, we may be missing the forest for the trees.

Also, I agree with the above post ^ that comptime makes sense as a modifier in multiple contexts, including expressions.

@Janrupf proposes: if comptime(some_fn()):

And in my previous post, one of the syntaxes I propose is: comptime if some_fn():

Which achieves what @parameter achieves today. Unlike @Janrupf's proposal, it doesn't rely on dead code analysis. Instead, it offers a rock-solid guarantee that the branch will be eliminated at compile-time. But I suppose Mojo could formally specify that the former syntax eliminates the branch, in which case, either syntax would suffice.

I think using comptime to evaluate expressions would be wonderful in other situations, such as the other example that @Janrupf gave: some_call(SOMETHING, comptime(do_it()), get_user_input())

augb commented 1 year ago

To summarize my view at this point in the discussion, I offer the following:

I started this as a replacement for the keyword alias; however, I'm seeing the benefit of aligning @parameter and alias under a single keyword such as const, compexp or comptime.

While I understand the choice of alias and @parameter, respectively, I feel these are too overloaded with meaning from other languages and common usage of these terms in programming.

If possible, having a single keyword, as opposed to a mix of a keyword and a decorator, would be preferable.

Mogball commented 1 year ago

This is veering off into discussion land. An issue is not the right place for something like this. Please start a new discussion (via Github discussions in this same repo) if you would like to continue this conversation.

Also FYI we already have compile-time ifs, in the form of @parameter if a + b

nmsmith commented 1 year ago

If Github issues aren't the appropriate place to discuss language features, then perhaps the "Feature Request" option should be removed from the list of issue templates. Because otherwise, I predict discussions such as this will be commonplace. (Because people are obviously going to respond to Feature Request issues with their thoughts about the feature.)

Mogball commented 1 year ago

That's probably a good idea. That said, certain feature requests are less controversial than others (e.g. implement __add__ on Python object or something). I suspect this one should have been "[RFC] Rename the alias keyword"

nmsmith commented 1 year ago

Perhaps the template should be "Missing feature" instead then. To stop people requesting a redesign of things that have already been implemented.

Mogball commented 1 year ago

Makes sense me to @goldiegadde. Although the problem will not go away. I suspect there will be more "please move this to an RFC" in the future.

augb commented 1 year ago

Discussion started in #190.