-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Module system #58
Comments
Not quite true. They can appear inside a namespace declaration or outside, a fact that has started many a style war over the years.
I wouldn't even say roughly. They are exactly static classes. Same with VB modules. Important to note in all of this that none of these correspond to what IL calls modules. Another note is that while IL certainly lets you define methods in namespaces, I don't think any .NET language would be able to access them (maybe C++/CLI could?). |
Fair, I've even used that at one of the workplaces, where it was convention to put My point there was to suggest that importing doesn't have to happen at only those levels, there is no reason not to allow importing in a function-local context.
Oh absolutely, we'd likely wrap up free-functions in static classes, or have the innermost module level be necessarily a static class. |
My issue with C# That's why I kinda like python/js imports systems, because you can use it how you like it:
The main difference is that python/JS work by importing by package, not namespace, without bringing the dependency explicitly, it allow you to be more explict for a little more cluter on top of the file, but less verbosity in the body. In practice, python packages management is so bad I can't even restore a project. For a new language that would not have any interop concern, I personally would go with the "you import packages, not namespace" option. Side note:
I think it would be sane to test if this doesn't create to too much conflicts caused by dependencies. Edit: some interesting discussion from discord:
About exports:
|
IMO the choice to somehow forget namespaces by binding them to the package name is not a good move. This basically couples the implementation to the "exposition": to use a Type, I need to know where it has been implemented. Up to me, this complicates evolution of complex code bases. A Type is semanticaly defined by its |
You can still expose Type defined in package A in package B. If you define Type |
The philosophy of namespaces here is implicit by nature, your namespace hierarchy is defined by structure. This was already a soft convention in C#. Namespaces essentially become package name + module hierarchy. For example, if I have this: // in file core.fr
module math
{
export matrices;
module matrices
{
export type mat4x4(...);
}
} Then effectively, the namespace of
That's semi-true! The exporting mechanism allows you to keep up a stable API file, similarly to JavaScript barrel files. The underlying implementation can change, files can be split or merged, but you can still re-export the module as the old API.
// We want to consume OmniSharp, we import parts of it
using OmniSharp. ...;
// For configuration, we need to grab some of the Nerdbank streams
using Nerdbank.Streams. ...; I see a problem with this: what if we want to change the implementation but keep the API stable? For example, we realize that But with an exporting mechanism, you could expose the dependency under your own package: // in some omnisharp module file
module streams
{
export * from Nerdbank.Streams;
} This means that internally you can change what you exported. Heck, you can roll your own implementation there, as long as the API stable. But in both cases, the access path and reference is |
You're totally right on the "double-edged". Now, if a project depends on both CK.Dapper and Dapper (typically through a transitivite dependency) it will not compile (CS0433 - The type 'SqlMapper' exists in both 'CK.Dapper' and 'Dapper') and this is a life savior: I don't want my code base to be able to run with 2 different implementations of the same beast at the same time. May be this discussion is all about private vs. transitive dependencies. For me, private dependencies are hell. This is my approach, just me trying to handle the numerous complexities of today's developer's job... |
That's totally understandable! I've shown in my previous post how you can actually hide where dependency elements come from and by that, you can replace them, essentially bringing them closer to private dependencies (while not being truly private, we are still in .NET world). You might say this is more cumbersome, I see this as a way to hide names (ability to alias) and only expose elements from the dependency you truly want to expose (which makes it even easier to replace I believe). It's also an opportunity to document and connect up the external dependencies your product uses, I see this as a good thing as well. |
An important note is that while it might be possible to do this easily at the source level, doing it at the binary level requires |
This proposal went through a major overhaul, completely redesigned it since the initial variations.
Introduction
This issue will try to lay down the module-system of the language, which mainly consists of the following ideas:
Note: Module systems are surprisingly hard to design in my opinion. They are often overlooked in language/compiler design. If I've missed something crucial, let me know.
A summary on other languages
I've attempted to put together a more complete summary of module systems in #73. This proposal also uses terminology from that issue, skimming through it is advised.
Proposal for Fresh
The proposal aims for something similar to the JavaScript or Rust module system, while fixing some of the mistakes or missing features.
Packages
Packages would be essentially NuGet packages/projects/assemblies, like the usual. The names can stay namespaced to reduce the chance of name collisions between packages. This means, that
Foo.Bar
is a completely valid package name.Modules
The module hierarchy would follow the directory structure, like in JavaScript or Rust. A file
foo.fr
would create a module namedfoo
. A file infoo/bar/baz.fr
would create the modulefoo.bar.baz
. This was already a soft convention with C# namespaces and the directory structure.Module folders can group their contribution under the folder's package name. For example, if a folder
math
has filesmarix.fr
,vector.fr
andquat.fr
, a file calledmodule.fr
insidemath
can provide a single module interface for all of these. This is very similar to JavaScript barrel files or mod.rs in Rust. (Note, that the file name is not permanent, we can use something shorter or completely different,module.fr
is just an initial idea).The outermost module (the top-level) would give the entire API interface of the entire package, which has an identical name to the package name.
Namespaces
The language has no direct concept of namespaces. This does not mean that there will not be namespaces when generating code, namespaces still exist on CLR level. This will be discussed in the interop section in more detail.
Exporting
Each module is responsible for exporting their own API towards other modules. Any symbol that should be accessible from the outside should be marked for exporting, similarly to JavaScript.
We could provide exporting inline, marking the elements directly, or as a separate export list. The export list could allow aliasing certain symbols:
Reexporting could also be supported (identical to the JavaScript feature, similar to
pub use
in Rust), which propagates another modules symbols upwards from the current module (especially useful inmodule.fr
-like API files):Note, that JavaScript syntax is used in the examples. We could get rid of the braces, as their sole purpose in JavaScript is to differentiate default imports/exports.
Module visibility
Modules are not visible by default, meaning that the following would cause an error:
They also need to be exported from their parent:
This gives the ability to control the visibility of submodules. With this feature, it's easy to hide that a given module is actually the accumulation of multiple other submodules and the consumer won't accidentally depend on such detail.
The exported modules of the main module will become part of the public API alongside the other symbols exported there.
Importing
Things can be accessed through their fully qualified name, but importing into local scope can be done with the
import
statement, similarly to JavaScript:Note, that
import some_module;
in JavaScript is only used to execute the side-effects of a module and I haven't found a use for it in this design, so currently that form of importing is unspecified.Note, that I don't want to take away the possibility to access everything by the fully qualified name, because this is a thing that would be heavily utilized by metaprogramming.
Member visibility
By default, type members would be not accessible from the outside. The members can be exported, just like other symbols to make them public. Note, that just like in Rust, this visibility is completely transitive, meaning that you don't need to keep re-exporting the type members to keep them externally accessible.
Interop with C#
Namespaces, module hierarchy
By default, the namespace of a module would simply be the name of the package name. The module hierarchy would be nested static classes. For example, if there is a module
foo
, which has two sub-modulesbar
andbaz
, all within packageHello.World
, in C# it would look like something like so:If required for interop, the namespaces or even the module class names could be controlled by attributes.
Visibility
The CLR has no concepts of import/export, only the standard visibilities of
public
/internal
/private
/... The symbols that are exported by the top-level module would bepublic
, the rest of the symbols would becomeinternal
. Theprotected
modifier would be recognized for interop, when extending an external .NET type.Interesting features
File-nested modules
Just like in Rust, submodules could be introduced without a hierarchy within the same file:
This is useful for metaprogramming, or for things like compressing a package into a single file.
Module extensions
Modules can be thought of as types with only static members. I believe there is no reason not to allow extending them, allowing for module extensions (syntax from #52):
The text was updated successfully, but these errors were encountered: