Crystal: Inference of type from namespace

Created on 13 Jan 2020  ยท  7Comments  ยท  Source: crystal-lang/crystal

When you declare a type with a namespaced name and the namespace has not been defined yet, the compiler automatically assumes it's a module.

module Foo::Bar
end

class Foo # Error: Foo is not a class, it's a module
end

The resulting error is inconvenient, because it's not really correct. Foo is technically supposed to be a class and it's only a module because the compiler decides so based on the order things are seen.
It's relatively easy to work around this, you just need to make sure the definitions are in the expected order. This however often leads to require sections at the bottom of a file which kind of hides them away or adding more files to get more granular control.

I was thinking if we could improve this situation. Basically, when the compiler sees a reference to Foo::Bar and doesn't already know Foo yet, the only explicit statement is that Foo is a namespace and contains Bar. We could add some kind of flag to signal that Foo's exact type is not known and in case we see an explicit definition like module Foo or struct Foo, it can be transformed to the respective type. Defaulting to module in case there is no such clarification is fine, obviously.

feature discussion topicsemantic

Most helpful comment

That's an unrelated visibility issue. Private constants are only visible inside the parent namespace, but your code references private, namespaced constant Athena::Routing::Param from the top-level namespace. It's just not visible there. This is just the same as:

private class Foo::Bar
end

Foo::Bar # Error: undefined constant Foo::Bar

In case you want to discuss changing this, please open a new issue (or check whether this has already been discussed before).

All 7 comments

It can easily happen when using require "somedir/**", when subdirectories are required before the *.cr subfiles.

Also frequently occurs when you follow the recommended file / directory structure and have nested classes, structs, enums etc in their own files.

$ tree .
.
โ”œโ”€โ”€ foo
โ”‚ย ย  โ”œโ”€โ”€ bar.cr
โ”‚   โ”œโ”€โ”€ baz.cr
โ”‚ย ย  โ””โ”€โ”€ qux.cr
โ””โ”€โ”€ foo.cr

foo.cr:

require "./foo/*
class Foo
  # ...
end

./foo/bar.cr:

class Foo::Bar
  # ...
end

Another alternative, which is much simpler to implement and probably won't have unexpected consequences (for example: what happens if you access that type using macros before it's finished) is to remove that "feature". If you do class Foo::Bar and Foo doesn't exist, you get an error. Exactly like in Ruby.

I added that feature because I thought it would be convenient for always using Foo as a module namespace (mainly for shards authors), but it seems people are using it for other types too.

So another alternative is to document this better, saying that this is meant to be used for modules, and just modules, mainly for shards namespaces.

I very much like the simplicity.

But regarding usability it would really be nice to have an easy solution to use cases as described in https://github.com/crystal-lang/crystal/issues/8685#issuecomment-583821411 Currently, you only have the option to move require "./foo/bar" at the end of the file (or at least after the first definition of Foo) or specify class Foo; class Bar explicitly in the required file. Both are not very elegant and I hope there can be a way to make this easier.

Probably related, https://play.crystal-lang.org/#/r/8lt0

private abstract struct Athena::Routing::Param; end

private record Athena::Routing::Parameter(T) < Athena::Routing::Param, value : T
Showing last frame. Use --error-trace for full trace.

There was a problem expanding macro 'record'


Called macro defined in /usr/lib/crystal/macros.cr:60:1

 60 | macro record(name, *properties)

Which expanded to:

 > 1 | struct Athena::Routing::Parameter(T) < Athena::Routing::Param
                                              ^---------------------
Error: undefined constant Athena::Routing::Param

Defining the structs within a module, or removing private fixes the issue.

That's an unrelated visibility issue. Private constants are only visible inside the parent namespace, but your code references private, namespaced constant Athena::Routing::Param from the top-level namespace. It's just not visible there. This is just the same as:

private class Foo::Bar
end

Foo::Bar # Error: undefined constant Foo::Bar

In case you want to discuss changing this, please open a new issue (or check whether this has already been discussed before).

I guess the error message is wrong, though. Or at least, it should be more specific and notify that a private constant was referenced, see #8831.

Was this page helpful?
0 / 5 - 0 ratings