risor-io/risor

Import `as` only works on `from x import y as z`

Closed this issue ยท 17 comments

The new import syntax introduced in #116 is super nice. Would be nice to have that rename capability on the regular import statement too

Such as:

import mylib as lib

Actually, thinking about it, could completely get rid of the from keyword and only rely on import.

from a.b.d import c as lib

# could be replaced by just doing this:
import a.b.c.d as lib

The from form feels appropriate when importing an attribute, e.g. from mymodule import someattr as a.

At the moment I lean towards keeping both from and import and supporting the rename capability on both.

Good point! It does fill a purpose.

However, in the current from x.y import z, it's kind of pulling double-duty. The current logic is:

  • Try import module x.y (x/y.risor), and find symbol z in there
  • Else, try import module x.y.z (x/y/z.risor) as an entire module

Maybe it'd be better to just remove that second logic, so the first from x.y MUST be a module (x/y.risor) and the second part import z MUST be a symbol in that module.

Then extending the plain import x.y.z would only translate to x/y/z.risor

So if you want a symbol from a module, then you MUST use from x import y statement, but if you want the entire module then you MUST use the plain import x.y statement.

Wdyt?

In general, I'm open to adjustments like this since we just added the feature. We should let @abadfox233 share their opinion as well.

I did merge #137 which added the alias support for the import statement.

I think we all agree on these points:

  • Support both from and import statements
  • Support aliases on both of those
  • from should support importing a specific attribute from a module

So I think the points to be decided on are these two:

  • Should from support importing modules (in addition to attributes), or should import only be used to import modules?
  • If from supports importing both modules and attributes, what is the order of precedence? Submodule, then attribute? Or attribute, then submodule?

My inclination at the moment is to have from support both behaviors, and just confirm the order of precedence is what we want. This matches Python's overall approach which is positive IMHO.

Looks like at the moment the order of precedence in Risor does not match Python, as a point of reference. Python first imports the attribute, then the submodule if no attribute is found. Risor at the moment imports the submodule, then the attribute if no submodule is found.

I believe that Risor's import precedence should align with Python's. Given Risor's design akin to Python's language, it's ideal to maintain import behavior similar to Python: allowing 'from' to support both attribute and module imports. Regarding the multi-level import support in 'import,' I also think it could mirror Python's behavior, supporting multi-level imports.

I don't see why Risor has to follow Python's design. Maybe for consistency?

It is a good idea to not have an opposite implementation of python (i.e opposite precedence), but it's not necessarily a good idea to adapt the same double-duty logic in the import and from-import statements. Just saying that we might want to question the design decisions of Python too.

Hmm, maybe I can find some discussions on why Python works as it does ๐Ÿค”.

Sidenote: why use Pythons import syntax, when so much else of Risor is inspired by Go?

I came across Risor's description on the website mentioning, 'Familiar: Friendly syntax for Go and Python developers.' That's why I assumed its behavior would mirror Python's. If there's no strict necessity for it to mimic Python's behavior, perhaps 'from' could exclusively support attribute imports while 'import' handles module imports. At the same time, I'm also interested in seeing Risor introduce features akin to Lua's tuples to implement object-oriented functionalities."

On second thought, I'm not a Python developer. So I should not have much say in this.

If you come from Python and see Risor's import and from-import statements, then is "import attributes/symbols with the plain import statement" valid expected behavior?

Personally it feels weird, but again I'm perhaps not the target audience here. And it's possible that Pythons precedence makes it so this is never a source of confusion.

Because what it sounds like is that if you have files lib.risor and lib/thing.risor, then in Python precedence the file lib/thing.risor would be loaded, except if lib.risor also defines a var thing = 123

Maybe this is a good thing, as then it could be possible to add extra logic on imports

Referencing Python was only intended to be a general direction and source of inspiration, not a requirement or rule.

I really don't want people like you @applejag to feel like you're not the target audience because of this. So I should really update the wording on the website and probably write down a few guiding principles for the language.

A short version of the guiding principle could be something like "a scripting language for the Go ecosystem, which is familiar to Go programmers but that is more concise and expressive than Go, drawing inspiration for extra language features from the latest developments in other loved languages such as Rust, Typescript, Zig, and Python."

If there's one language we should stay closest to, when it doesn't hurt the language's expressiveness or effectiveness for scripting, it's Go. So I think we should shift the discussion of imports to be more in this direction: what's the best syntax and functionality for imports that will make sense to Go developers but also be well-suited for a scripting language?

I appreciate you all @abadfox233 and @applejag working with me on this and helping me write down a better set of rules and guidance on all this.

What do you think?

At the same time, I'm also interested in seeing Risor introduce features akin to Lua's tuples to implement object-oriented functionalities."

@abadfox233 - I do think Risor needs some form of user-defined types, that would support object-oriented programming to some degree. This could be similar to Go structs, Typescript interfaces, or something else.

I just created this discussion thread to dive into this more deeply:
#138

Very nice words @myzie. You're making this a very inclusive forum.

Comparing with Go in this case for the import semantics is a bit tricky, as Go has their concept of packages that Risor doesn't have. Having Go package semantics for scripting isn't a good fit. Importing on a file basis is the way to go.

Going from importing packages based on module path to importing files based on relative path isn't that big of a leap.

However, a big foreign concept compared to Go is the "import a symbol" thing. Because of this, I think it should be very explicit when that is used.

Go celebrates itself for having a very limited set of "hidden control flow" (e.g no throwing exceptions, no function overloads, no operator overloading, for range loops only works on slices). I think this is the mindset that Risor should consider the most. Maybe not follow, but at least consider.


Another observation is that Go imports use string syntax, while Risor currently has its own syntax.

Don't know if it would make sense to mimic that? Just thinking out loud here, but if Risor were to go for a string syntax, then having it import symbols based on the string would definitely not be appropriate. On the other hand, it might read better then as to what is code/symbol references and what is a path. Such as:

import "lib"
from "lib" import abc

# maybe replace dot notation with slashes then too
import "lib/x/y/z"
from "lib/x/y/z" import foo

# would it make sense to also add the file extension then?
import "lib.risor"
import "lib.rsr" # no ambiguity with lib.risor

Making it a string could also make the parser simpler, and could make a distinction between built-in module and local module, by forcing a ./ prefix, such as:

import "fmt" # built-in module
import "./fmt.risor" # local file

These are severely breaking changes, so need to be careful here. Unless Risor were to support both syntaxes (with strings or with dot-separated identifiers).

A short version of the guiding principle could be something like "a scripting language for the Go ecosystem, which is familiar to Go programmers but that is more concise and expressive than Go, drawing inspiration for extra language features from the latest developments in other loved languages such as Rust, Typescript, Zig, and Python."

And this definition looks good to me.

I think we don't need to constrain Risor. Since there are no obvious drawbacks, let's simply support these features. The only thing below is the kind of import syntax that feels more comfortable.

I believe there are some issues worth discussing regarding Risor's package importing at the moment:

  1. Whether to support searching from the current path when importing packages, as it currently only supports searching from the source code directory.
  2. Whether there's a need to identify and report errors for circular imports
  1. I think it should always be relative to the file itself.
  2. I would expect that the Risor VM caches the files. So importing twice returns the same compiled file. Then Risor should prevent circular dependencies so partially loaded files are not imported.

For point 2, imagine this:

// file1.risor

import "file2"

func my_func() {}
// file2.risor

import "file1" // possible error: illegal circular import 

file1.my_func() // possible error: unknown symbol "my_func"

In the above, it either disallows circular import (I vote this), does some magic to allow this (not sure how that would work as Risor allows top-level statements), or results in partially imported "file1.risor" where "my_func" is not available yet, as it's referenced before it's definition after "file2.risor" has completely been imported.

@abadfox233 what's your point of view on:

    1. import statement using string paths (import "foo/bar") vs using identifiers (import foo.bar)
    1. if string, then with extension (import "foo.risor") or without (import "foo")
    1. forcing distinction between built-in module (import "fmt") and local file module (import "./fmt" or import "./fmt.risor")

(Numbering these differently to not confuse with your numbered list)

Collectively our conversation did a lot of meandering in here. The origin purpose of the ticket has been fulfilled, which was supporting as in import statements.

I think as-is the import and from-import statements work fairly well and cover most use cases. Let's use it some the way it is and see how it does in practice. We can consider specific incremental improvements but it's not clear to me we need a big change to that way it works now.

Fare