Module questions / "interface" functionality

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:

  1. 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.

  1. “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!

2 Likes
  1. 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:
// ParentModule.res
module SubModule = {
  //...
}
module AnotherOne = {
  //...
}

If the sub modules end up becoming particularly large, one convention is to name them with their parent’s name separated by spaces:

ParentModule.res
ParentModule/
| ParentModule__SubModule.res
| ParentModule__AnotherOne.res

and then import the submodules into the file:

// ParentModule.res
module Submodule = ParentModule__SubModule
module AnotherOne = ParentModule__AnotherOne
  1. 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)
4 Likes

Thank you for this! This solves my dilemma. I appreciate the response.

2 Likes

Hello @elias-michaias and welcome to ReScript!

Thank you for your kind word about ReScript. :slight_smile:

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
4 Likes

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.

1 Like