suggestions
suggestions copied to clipboard
Private modules/contexts within a project
The ability to define a module which can only be used by a subset of the other modules in a project
Some prior-art for this is the Elixir library Boundary: https://github.com/sasa1977/boundary
The model doesn't map to Gleam perfectly though, because Gleam's module hierarchy is different from how Elixir codebases are typically structured, but that readme includes good examples of use-cases.
A closer example to how Gleam works might be the way packages work in Java. Classes can define a package, and then classes with no access modifier are only accessible to other classes within their package.
One way this might look in Gleam is:
// inside src/foo.gleam
private package MyPackage
fn hello() {
"hello world!"
}
// inside src/bar.gleam
public package MyPackage
import "foo"
fn world() {
foo.hello()
}
In this example, "foo" is a private module in the MyPackage package, and can only be called by other modules in that package. "bar" belongs to the same package, but is public, and accessible to all other modules.
Similarly, this snippet would fail, because "baz" is not in the same package as "foo":
// inside src/baz.gleam
import "foo"
fn world() {
foo.hello()
}
Likely, packages should not be required on a module, and a module belonging to no package is simply considered public, and without access to any private packaged modules.
Thanks for the information! I like the idea of dividing a Gleam project into parts, each with their own private and public modules.
One thing I am not sure of is how the name "package" fits in as we already use the name "package" when talking about Hex, and I feel that would probably map most clearly onto the entire project. I would like us to carefully pick names to avoid any confusion if possible.
I agree with being very careful about naming. How do you feel about something like modulegroup or modulespace?
I kind of like the parallel between modulespace and namespace: it's a collection of closely related modules.
Is this a place to also think about "opaque" types? I would like to return types to the caller of a function that they should not be able to destructure, instead only use functions on the module. This would be because there are other consistency guarantees I want on the data in the type that can't be guaranteed by the type system, and so it's best for the module that "owns" the type to be the only one that can modify it
I think this is probably similar to rust struct visibility, although I can only really think of cases where all/none of the fields should be visible
Yes definitely Peter! I can't decide on a syntax though for that one. Ideas very much welcome
Proposal
Arguments
I was thinking that if you want to enforce semver then it's probably a good idea to encourage libraries to have a small an interface as possible. Being forced to break semver because an inner module that you never intended to expose has changed could be annoying.
Therefore I like the idea of default private modules.
Functions and Types are also private by default, so extending this idea to default private modules seems to fit in.
I also like being able to re-export functions on modules, for example json.decode where the decode function is actually defined in src/json/decoder.
Suggestion
The top level of a module is always available In the top level file
// module available in this file but not exported
import lib/foo
// module can be called by external code (import lib/bar; bar.my_func())
pub import lib/bar
// module can be called by external code (import lib/helpers; helpers.my_func())
pub import lib/utils as helpers
// functions are available on top level module. (import lib; lib.my_func())
pub import lib/decoder.{decode}
pub import lib/encoder.{encode}
In summary add pub before an import to make a submodule available. This can work recursively. For example in bar.gleam
pub import lib/bar/subbar
now users of the library can do import lib/bar/subbar
This proposal doesn't fit well with Gleam at present because we don't have a middle system in which a project name is the root and everything is nested from there. We could possibly change the module system to work like Rust's for this, though I may want to use a different keyword for declaring a module than for importing one.
First Point,
it's normally a good idea to only have one top level module, particularly in erlangs global namespace for modules, I wouldn't be against enforcing this. But I take you point it doesn't exist yet.
- All modules that are at the top level of the src directory could be public?
- Gleam.toml could specify the first public module(s)
Second Point
I assume you meant a separate key word to declaring modules public than importing.
I like the consistency I see with pub fn and fn vs pub import and import. Potentially you could separate them
pub export lib/foo
import lib/foo
// "pub import" could just be sugar to do both the above steps, but doing so stops you being able to search the source for the word export.
pub import lib/foo
it's normally a good idea to only have one top level module
This is the approach in Elixir and Ruby but is not the approach in Elm, Haskell or PureScript. It's a matter of convention rather than being objectively better.
If we are to adopt that convention I would probably want to enforce it by always adding a prefix the name of the package to the module name.
That would mean that gleam_stdlib/src/gleam/list.gleam becomes gleam_stdlib/src/list.gleam and import gleam/list becomes import gleam_stdlib/list.
If we do this we would need to update all existing libraries other than midas from category based naming to package based naming, we would need to teach the compiler about packages (possibly a good idea), moving modules between packages becomes a break change that requires more work to fix on the user side, and we would no longer be able to have top level modules such as midas.
In exchange we remove the possibility of having module name collisions between different packages.
One to think about more I think, I'm not seeing a clear win here.