Hello all!
Huge fan of this wonderful programming language so far. I absolutely love it.
I do have a couple questions about the optimal path forward on a couple of module-related issues - please excuse me if these are considered trivial. I don’t have prior OCaml experience so my grasp on modules in general is going to be lower than some other peoples’. Here are the issues I’m having:
Multiple files under one module
Is it possible to spread out a module across files without using something like include ...? I’d love the ability to do this:
ParentModule/
| SubModule.res
| AnotherOne.res
And then access things like ParentModule.SubModule.foo.
“Bare minimum” interface functionality
I’d really like to be able to enforce a minimum compliance for modules using a module type, but still allow for any types/functions defined that are defined in said module that are “extra” (with respect to the module type) still be publicly accessible/exported. Is this possible? This is what I’m referring to:
module type Foo = {
type t = string
}
module Bar: Foo = {
type t = string
// "extra" functionality
let baz = x => x + 2
}
Bar.baz(4)->ignore
// ERROR: can't find Bar.baz
Is this simply just considered an anti-pattern in ReScript? Would it be recommended to always use the interface on a minimum-compliance submodule like so:
module type Foo = {
type t = string
}
module Bar = {
module Spec: Foo = {
type t = string
}
// "extra" functionality
let baz = x => x + 2
}
Bar.baz(4)->ignore
I would really appreciate any help navigating these questions. It’s entirely possible I’m just reaching for the wrong tool - so just let me know. Thank you all in advance. This is a truly wonderful programming language and I’m so very excited to use it!
To have the module hierarchy, the submodules would actually need to be defined inside of a parent ParentModule file. For simple cases, just doing it inline usually suffices:
When you annotate the type of a module, you’re actually setting its public interface, hence why your first example causes the compiler to complain. If instead you want to have some minimum requirement, you could exploit the duck-typing of module parameters to force the compiler to check that your module satisfies the interface:
module type Foo = {
type t = string
}
module IsFoo = (F: Foo) => {}
module Bar = {
type t = string
// "extra" functionality
let baz = x => x + 2
}
// This will complain if Bar fails to satisfy Foo's interface
module BarIsFoo = IsFoo(Bar)
For your first question, another way to solve it is to break down your main modules into packages inside a monorepo and use namespace in rescript.json.
{
// ...
"namespace": "MyLib1"
// ...
}
For your second question, another solution is to include the module type:
module type Foo = {
type t = string
}
module Bar: {
include Foo
let baz: int => int
} = {
type t = string
// "extra" functionality
let baz = x => x + 2
}
Bar.baz(4)->ignore
// no error anymore
The trick for No. 2 is probably the most ideal solution. For No. 1, does that require me making the package remote? Or can I import a local package? Furthermore, if I do this, since it is an “external package” will those types not be exported in my project if I publish my project as a package of its own, since those modules don’t belong to my package per se? If you have any example repos implementing this strategy, that would be very helpful for me. Thank you for the helpful response!
For #1, you can have a monorepo setup where all your packages are local with a root rescript.json, v12 built-in build system handles it out of the box.
I think you’ll then need to install and set up all the packages in rescript.json, so not always ideal, but can be very convenient for more modular/granular coverage.
I think rescript-mui is a pretty good example of such monorepos.