title | description | canonical |
---|---|---|
Module | ReScript modules, module signatures and interface files | /docs/manual/v12.0.0/module |
Modules are like mini files! They can contain type definitions, let
bindings, nested modules, etc.
To create a module, use the module
keyword. The module name must start with a capital letter. Whatever you could place in a .res
file, you may place inside a module definition's {}
block.
<CodeTab labels={["ReScript", "JS Output"]}>
moduleSchool= { typeprofession=Teacher | Directorletperson1=TeacherletgetProfession= (person) =>switchperson { | Teacher=>"A teacher" | Director=>"A director" } }
functiongetProfession(person){if(person){return"A director";}else{return"A teacher";}}varSchool={person1: /* Teacher */0,getProfession: getProfession};
A module's contents (including types!) can be accessed much like a record's, using the .
notation. This demonstrates modules' utility for namespacing.
<CodeTab labels={["ReScript", "JS Output"]}>
letanotherPerson: School.profession=School.TeacherConsole.log(School.getProfession(anotherPerson)) /* "A teacher" */
varanotherPerson=/* Teacher */0;console.log("A teacher");
Nested modules work too.
<CodeTab labels={["ReScript", "JS Output"]}>
moduleMyModule= { moduleNestedModule= { letmessage="hello" } } letmessage=MyModule.NestedModule.message
varNestedModule={message: message};varMyModule={NestedModule: NestedModule};varmessage=MyModule.NestedModule.message;
Constantly referring to a value/type in a module can be tedious. Instead, we can "open" a module and refer to its contents without always prepending them with the module's name. Instead of writing:
<CodeTab labels={["ReScript", "JS Output"]}>
letp=School.getProfession(School.person1)
varp=School.getProfession(School.person1);
We can write:
<CodeTab labels={["ReScript", "JS Output"]}>
openSchoolletp=getProfession(person1)
varp=School.getProfession(School.person1);
The content of School
module are made visible (not copied into the file, but simply made visible!) in scope. profession
, getProfession
and person1
will thus correctly be found.
Use open
this sparingly, it's convenient, but makes it hard to know where some values come from. You should usually use open
in a local scope:
<CodeTab labels={["ReScript", "JS Output"]}>
letp= { openSchoolgetProfession(person1) } /* School's content isn't visible here anymore */
varp=School.getProfession(School.person1);
There are situations where open
will cause a warning due to existing identifiers (bindings, types) being redefined. Use open!
to explicitly tell the compiler that this is desired behavior.
letmap= (arr, value) => { value } // opening Array would shadow our previously defined `map`// `open!` will explicitly turn off the automatic warningopen!Arrayletarr=map([1,2,3], (a) => { a+1})
Note: Same as with open
, don't overuse open!
statements if not necessary. Use (sub)modules to prevent shadowing issues.
Since 9.0.2
As an alternative to open
ing a module, you can also destructure a module's functions and values into separate let bindings (similarly on how we'd destructure an object in JavaScript).
<CodeTab labels={["ReScript", "JS Output"]}>
moduleUser= { letuser1="Anna"letuser2="Franz" } // Destructure by namelet {user1, user2} =module(User) // Destructure with different aliaslet {user1: anna, user2: franz} =module(User)
varuser1="Anna";varuser2="Franz";varUser={user1: user1,user2: user2};
Note: You can't extract types with module destructuring — use a type alias instead (type user = User.myUserType
).
Using include
in a module statically "spreads" a module's content into a new one, thus often fulfill the role of "inheritance" or "mixin".
Note: this is equivalent to a compiler-level copy paste. We heavily discourage include
. Use it as last resort!
<CodeTab labels={["ReScript", "JS Output"]}>
moduleBaseComponent= { letdefaultGreeting="Hello"letgetAudience= (~excited) =>excited ? "world!" : "world" } moduleActualComponent= { /* the content is copied over */includeBaseComponent/* overrides BaseComponent.defaultGreeting */letdefaultGreeting="Hey"letrender= () =>defaultGreeting++" "++getAudience(~excited=true) }
functiongetAudience(excited){if(excited){return"world!";}else{return"world";}}varBaseComponent={defaultGreeting: "Hello",getAudience: getAudience};vardefaultGreeting="Hey";functionrender(param){return"Hey world!";}varActualComponent={getAudience: getAudience,defaultGreeting: defaultGreeting,render: render};
Note: open
and include
are very different! The former brings a module's content into your current scope, so that you don't have to refer to a value by prefixing it with the module's name every time. The latter copies over the definition of a module statically, then also do an open
.
Every ReScript file is itself compiled to a module of the same name as the file name, capitalized. The file React.res
implicitly forms a module React
, which can be seen by other source files.
Note: ReScript file names should, by convention, be capitalized so that their casing matches their module name. Uncapitalized file names are not invalid, but will be implicitly transformed into a capitalized module name. I.e. file.res
will be compiled into the module File
. To simplify and minimize the disconnect here, the convention is therefore to capitalize file names.
A module's type is called a "signature", and can be written explicitly. If a module is like a .res
(implementation) file, then a module's signature is like a .resi
(interface) file.
To create a signature, use the module type
keyword. The signature name must start with a capital letter. Whatever you could place in a .resi
file, you may place inside a signature definition's {}
block.
<CodeTab labels={["ReScript", "JS Output"]}>
/* Picking up previous section's example */moduletype EstablishmentType= { typeprofessionletgetProfession: profession=>string }
// Empty output
A signature defines the list of requirements that a module must satisfy in order for that module to match the signature. Those requirements are of the form:
let x: int
requires alet
binding namedx
, of typeint
.type t = someType
requires a type fieldt
to be equal tosomeType
.type t
requires a type fieldt
, but without imposing any requirements on the actual, concrete type oft
. We'd uset
in other entries in the signature to describe relationships, e.g.let makePair: t => (t, t)
but we cannot, for example, assume thatt
is anint
. This gives us great, enforced abstraction abilities.
To illustrate the various kinds of type entries, consider the above signature EstablishmentType
which requires that a module:
- Declare a type named
profession
. - Must include a function that takes in a value of the type
profession
and returns a string.
Note:
Modules of the type EstablishmentType
can contain more fields than the signature declares, just like the module School
in the previous section (if we choose to assign it the type EstablishmentType
. Otherwise, School
exposes every field). This effectively makes the person1
field an enforced implementation detail! Outsiders can't access it, since it's not present in the signature; the signature constrained what others can access.
The type EstablishmentType.profession
is abstract: it doesn't have a concrete type; it's saying "I don't care what the actual type is, but it's used as input to getProfession
". This is useful to fit many modules under the same interface:
<CodeTab labels={["ReScript", "JS Output"]}>
moduleCompany: EstablishmentType= { typeprofession=CEO | Designer | Engineer | ...letgetProfession= (person) =>...letperson1=...letperson2=... }
functiongetProfession(person){ ... }varperson1= ... varperson2= ... varCompany={getProfession: getProfession,person1: person1,person2: person2};
It's also useful to hide the underlying type as an implementation detail others can't rely on. If you ask what the type of Company.profession
is, instead of exposing the variant, it'll only tell you "it's Company.profession
".
Like modules themselves, module signatures can also be extended by other module signatures using include
. Again, heavily discouraged:
<CodeTab labels={["ReScript", "JS Output"]}>
moduletype BaseComponent= { letdefaultGreeting: stringletgetAudience: (~excited: bool) =>string } moduletype ActualComponent= { /* the BaseComponent signature is copied over */includeBaseComponentletrender: unit=>string }
// Empty output
Note: BaseComponent
is a module type, not an actual module itself!
If you do not have a defined module type, you can extract it from an actual module using include (module type of ActualModuleName)
. For example, we can extend the List
module from the standard library, which does not define a module type.
<CodeTab labels={["ReScript", "JS Output"]}>
moduletype MyList= { include (moduletype of List) letmyListFun: list<'a> =>list<'a> }
// Empty output
Similar to how a React.res
file implicitly defines a module React
, a file React.resi
implicitly defines a signature for React
. If React.resi
isn't provided, the signature of React.res
defaults to exposing all the fields of the module. Because they don't contain implementation files, .resi
files are used in the ecosystem to also document the public API of their corresponding modules.
<CodeTab labels={["ReScript", "JS Output"]}>
/* file React.res (implementation. Compiles to module React) */typestate=intletrender= (str) =>str
functionrender(str){returnstr;}
/* file React.resi (interface. Compiles to the signature of React.res) */typestate=intletrender: string=>string
Modules can be passed to functions! It would be the equivalent of passing a file as a first-class item. However, modules are at a different "layer" of the language than other common concepts, so we can't pass them to regular functions. Instead, we pass them to special functions called "functors".
The syntax for defining and using functors is very much like the syntax for defining and using regular functions. The primary differences are:
- Functors use the
module
keyword instead oflet
. - Functors take modules as arguments and return a module.
- Functors require annotating arguments.
- Functors must start with a capital letter (just like modules/signatures).
Here's an example MakeSet
functor, that takes in a module of the type Comparable
and returns a new set that can contain such comparable items.
<CodeTab labels={["ReScript", "JS Output"]}>
moduletype Comparable= { typetletequal: (t, t) =>bool } moduleMakeSet= (Item: Comparable) => { // let's use a list as our naive backing data structuretypebackingType=list<Item.t> letempty=list{} letadd= (currentSet: backingType, newItem: Item.t): backingType=>// if item existsifcurrentSet->List.some(x=>Item.equal(x, newItem)) { currentSet// return the same (immutable) set (a list really) } else { list{ newItem, ...currentSet// prepend to the set and return it } } }
varList=require("./stdlib/list.js");functionMakeSet(Item){varadd=function(currentSet,newItem){if(List.exists(function(x){returnItem.equal(x,newItem);},currentSet)){returncurrentSet;}else{return{hd: newItem,tl: currentSet,};}};return{empty: /* [] */0,add: add,};}
Functors can be applied using function application syntax. In this case, we're creating a set, whose items are pairs of integers.
<CodeTab labels={["ReScript", "JS Output"]}>
moduleIntPair= { typet= (int, int) letequal= ((x1: int, y1: int), (x2, y2)) =>x1==x2&&y1==y2letcreate= (x, y) => (x, y) } /* IntPair abides by the Comparable signature required by MakeSet */moduleSetOfIntPairs=MakeSet(IntPair)
functionequal(param,param$1){if(param[0]===param$1[0]){returnparam[1]===param$1[1];}else{returnfalse;}}functioncreate(x,y){return[x,y];}varIntPair={equal: equal,create: create,};varSetOfIntPairs={empty: /* [] */0,add: add,};
Like with module types, functor types also act to constrain and hide what we may assume about functors. The syntax for functor types are consistent with those for function types, but with types capitalized to represent the signatures of modules the functor accepts as arguments and return values. In the previous example, we're exposing the backing type of a set; by giving MakeSet
a functor signature, we can hide the underlying data structure!
<CodeTab labels={["ReScript", "JS Output"]}>
moduletype Comparable=...moduletype MakeSetType= (Item: Comparable) => { typebackingTypeletempty: backingTypeletadd: (backingType, Item.t) =>backingType } moduleMakeSet: MakeSetType= (Item: Comparable) => { ... }
// Empty output
Since 8.3
It is possible to use non-conventional characters in your filenames (which is sometimes needed for specific JS frameworks). Here are some examples:
src/Button.ios.res
pages/[id].res
Please note that modules with an exotic filename will not be accessible from other ReScript modules.
Modules and functors are at a different "layer" of language than the rest (functions, let bindings, data structures, etc.). For example, you can't easily pass them into a tuple or record. Use them judiciously, if ever! Lots of times, just a record or a function is enough.