gleam-lang / gleam

⭐️ A friendly language for building type-safe, scalable systems!
https://gleam.run
Apache License 2.0
16.7k stars 698 forks source link

Importing types in a cycle should be allowed #2949

Closed m93a closed 3 months ago

m93a commented 3 months ago

Assume I have these types:

pub type Present {
  Contents(String)
}
pub type Bag {
  EmptyBag
  BoxInBag(Box)
}
pub type Box {
  EmptyBox
  PresentInBox(Present)
  BoxInBox(Box)
  BagInBox(Bag)
}

Notice Box and Bag are mutually dependent – you can put a Box in a Bag and vice versa. Now imagine that the codebase grows and both types get several associated functions. It might be a good idea to split them up into separate files. However, if I try, Gleam throws an error:

error: Import cycle

The import statements for these modules form a cycle:

    ┌─────┐
    │     bag
    │     ↓
    │     box
    └─────┘
Gleam doesn't support dependency cycles like these, please break the
cycle to continue.

This is quite a serious limitation, because from my experience, mutually dependent data types are the norm rather than an exception. This is also why TypeScript added type-only imports, for example.

It would be great if Gleam added support for import cycles in type-only imports too, so that we don't have to put all mutually dependent types into a one huge file.

lpil commented 3 months ago

Hello!

Thank you, but they are not permitted by design. It is my opinion that an import cycle is likely the sign of an unclear design where the domains of your program have unclear boundaries, or the sign that your code has been split into multiple modules when the contents are really of the same domain and would benefit from being in the same module.

Further permitting import cycles would negatively impact compile times, and we value the developer experience of a fast feedback loop.

m93a commented 3 months ago

Hey Louis!

It is my opinion that an import cycle is likely the sign of an unclear design

With all respect, I strongly disagree with your opinion. For example, if you're creating types for an AST (which I currently am), the bags-and-boxes situation is inevitable: every reasonable language has expressions of various types, which in turn contain more expressions, in a recursive manner. The same applies for an intermediate representation for a type system, a DOM, or more generaly any tree structure.

My first attempt at organizing such a codebase would be to create a common folder for all the related data types (called eg. ast) and put all the interdependent types into separate files there. I would argue that code organized like this would be much easier to read and maintain than a single huge file.

Another benefit of separating the code into files would be better namespacing, as instead of having tens of functions named emit_code_call_expression, emit_code_binary_operation_expression, emit_code_literal_expression, ... I could have just emit_code for each type, distinguished by qualified import, just like Gleam's standard library does it. As separating to different files is currently the only way to do namespacing in Gleam, I would say that not being able to use it is currently quite limiting.

Further permitting import cycles would negatively impact compile times

I'm not a programming languages expert, so I probably don't know what I'm talking about, but shouldn't the speed of compiling several interdependent files be roughly the same as if they were concatenated into one large file? Since concatenating the code into one large file is what we have to do manually anyway, I don't think I understand how this would negatively impact the compilation.

Of course, I respect whatever direction you chose for the language – I just thought I'd leave you with my sincere feedback from using it. Right now, I'll try to use the one-big-file approach for my project, and if it becomes unbearable, I'll change to a more suitable language.

Best regards, and thanks for making Gleam, otherwise it's quite a fun language o:)

lpil commented 3 months ago

My first attempt at organizing such a codebase would be to create a common folder for all the related data types (called eg. ast) and put all the interdependent types into separate files there. I would argue that code organized like this would be much easier to read and maintain than a single huge file.

Using a single file is the recommendation here. Making the programmer switch between files doesn't help any particular way, and it makes it harder to make clear what is an is not appropriate to import into other modules.