From 381e430564011b73598316104c3dffacc1a439e4 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Fri, 5 Sep 2025 10:17:20 +0200 Subject: [PATCH 1/3] Copy module function guide to v12 docs --- data/sidebar_manual_v1200.json | 1 + .../docs/manual/v12.0.0/module-functions.mdx | 336 ++++++++++++++++++ 2 files changed, 337 insertions(+) create mode 100644 pages/docs/manual/v12.0.0/module-functions.mdx diff --git a/data/sidebar_manual_v1200.json b/data/sidebar_manual_v1200.json index 29f70d21b..fe2dea8b8 100644 --- a/data/sidebar_manual_v1200.json +++ b/data/sidebar_manual_v1200.json @@ -61,6 +61,7 @@ "Advanced Features": [ "extensible-variant", "scoped-polymorphic-types", + "module-functions", "generalized-algebraic-data-types" ] } diff --git a/pages/docs/manual/v12.0.0/module-functions.mdx b/pages/docs/manual/v12.0.0/module-functions.mdx new file mode 100644 index 000000000..8f84161cd --- /dev/null +++ b/pages/docs/manual/v12.0.0/module-functions.mdx @@ -0,0 +1,336 @@ +--- +title: "Module Functions" +description: "Module Functions in ReScript" +canonical: "/docs/manual/v12.0.0/module-functions" +--- + +# Module Functions + +Module functions can be used to create modules based on types, values, or functions from other modules. +This is a powerful tool that can be used to create abstractions and reusable code that might not be possible with functions, or might have a runtime cost if done with functions. + +This is an advanced part of ReScript and you can generally get by with normal values and functions. + +## Quick example + +Next.js has a `useParams` hook that returns an unknown type, +and it's up to the developer in TypeScript to add a type annotation for the parameters returned by the hook. + +```TS +const params = useParams<{ tag: string; item: string }>() +``` + +In ReScript we can create a module function that will return a typed response for the `useParams` hook. + + +```res example +module Next = { + // define our module function + module MakeParams = (Params: { type t }) => { + @module("next/navigation") + external useParams: unit => Params.t = "useParams" + /* You can use values from the function parameter, such as Params.t */ + } +} + +module Component: { +@react.component +let make: unit => Jsx.element +} = { +// Create a module that matches the module type expected by Next.MakeParams +module P = { +type t = { +tag: string, +item: string, +} +} + +// Create a new module using the Params module we created and the Next.MakeParams module function +module Params = Next.MakeParams(P) + +@react.component +let make = () => { +// Use the functions, values, or types created by the module function +let params = Params.useParams() + +
+

+{React.string("Tag: " ++ params.tag /_ params is fully typed! _/)} +

+

{React.string("Item: " ++ params.item)}

+
+} +} + +```` +```js +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as $$Navigation from "next/navigation"; +import * as JsxRuntime from "react/jsx-runtime"; + +function MakeParams(Params) { + return {}; +} + +var Next = { + MakeParams: MakeParams +}; + +function Playground$Component(props) { + var params = $$Navigation.useParams(); + return JsxRuntime.jsxs("div", { + children: [ + JsxRuntime.jsx("p", { + children: "Tag: " + params.tag + }), + JsxRuntime.jsx("p", { + children: "Item: " + params.item + }) + ] + }); +} + +var Component = { + make: Playground$Component +}; + +export { + Next , + Component , +} +/* next/navigation Not a pure module */ + +```` + + + +## Sharing a type with an external binding + +This becomes incredibly useful when you need to have types that are unique to a project but shared across multiple components. +Let's say you want to create a library with a `getEnv` function to load in environment variables found in `import.meta.env`. + +```res +@val external env: 'a = "import.meta.env" + +let getEnv = () => { + env +} +``` + +It's not possible to define types for this that will work for every project, so we just set it as 'a and the consumer of our library can define the return type. + +```res +type t = {"LOG_LEVEL": string} + +let values: t = getEnv() +``` + +This isn't great and it doesn't take advantage of ReScript's type system and ability to use types without type definitions, and it can't be easily shared across our application. + +We can instead create a module function that can return a module that has contains a `getEnv` function that has a typed response. + +```res +module MakeEnv = ( + E: { + type t + }, +) => { + @val external env: E.t = "import.meta.env" + + let getEnv = () => { + env + } +} +``` + +And now consumers of our library can define the types and create a custom version of the hook for their application. +Notice that in the JavaScript output that the `import.meta.env` is used directly and doesn't require any function calls or runtime overhead. + + +```res +module Env = MakeEnv({ + type t = {"LOG_LEVEL": string} +}) + +let values = Env.getEnv() + +```` +```js +var Env = { + getEnv: getEnv +}; + +var values = import.meta.env; +```` + + + +## Shared functions + +You might want to share functions across modules, like a way to log a value or render it in React. +Here's an example of module function that takes in a type and a transform to string function. + +```res +module MakeDataModule = ( + T: { + type t + let toString: t => string + }, +) => { + type t = T.t + let log = a => Console.log("The value is " ++ T.toString(a)) + + module Render = { + @react.component + let make = (~value) => value->T.toString->React.string + } +} +``` + +You can now take a module with a type of `t` and a `toString` function and create a new module that has the `log` function and the `Render` component. + + +```res +module Person = { + type t = { firstName: string, lastName: string } + let toString = person => person.firstName ++ person.lastName +} + +module PersonData = MakeDataModule(Person) + +```` + +```js +// Notice that none of the JS output references the MakeDataModule function + +function toString(person) { + return person.firstName + person.lastName; +} + +var Person = { + toString: toString +}; + +function log(a) { + console.log("The value is " + toString(a)); +} + +function Person$MakeDataModule$Render(props) { + return toString(props.value); +} + +var Render = { + make: Person$MakeDataModule$Render +}; + +var PersonData = { + log: log, + Render: Render +}; +```` + + + +Now the `PersonData` module has the functions from the `MakeDataModule`. + + +```res +@react.component +let make = (~person) => { + let handleClick = _ => PersonData.log(person) +
+ {React.string("Hello ")} + + +
+} +``` +```js +function Person$1(props) { + var person = props.person; + var handleClick = function (param) { + log(person); + }; + return JsxRuntime.jsxs("div", { + children: [ + "Hello ", + JsxRuntime.jsx(Person$MakeDataModule$Render, { + value: person + }), + JsxRuntime.jsx("button", { + children: "Log value to console", + onClick: handleClick + }) + ] + }); +} +``` +
+ +## Dependency injection + +Module functions can be used for dependency injection. +Here's an example of injecting in some config values into a set of functions to access a database. + + +```res +module type DbConfig = { + let host: string + let database: string + let username: string + let password: string +} + +module MakeDbConnection = (Config: DbConfig) => { +type client = { +write: string => unit, +read: string => string, +} +@module("database.js") +external makeClient: (string, string, string, string) => client = "makeClient" + +let client = makeClient(Config.host, Config.database, Config.username, Config.password) +} + +module Db = MakeDbConnection({ +let host = "localhost" +let database = "mydb" +let username = "root" +let password = "password" +}) + +let updateDb = Db.client.write("new value") + +```` +```js +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as DatabaseJs from "database.js"; + +function MakeDbConnection(Config) { + var client = DatabaseJs.makeClient(Config.host, Config.database, Config.username, Config.password); + return { + client: client + }; +} + +var client = DatabaseJs.makeClient("localhost", "mydb", "root", "password"); + +var Db = { + client: client +}; + +var updateDb = client.write("new value"); + +export { + MakeDbConnection , + Db , + updateDb , +} +/* client Not a pure module */ +```` + + From 8217fc13fc686249d3f6358a72ca3dbfae177d39 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Fri, 5 Sep 2025 10:17:31 +0200 Subject: [PATCH 2/3] Copy gadt guide to v11 docs --- data/sidebar_manual_v1100.json | 3 +- .../generalized-algebraic-data-types.mdx | 300 ++++++++++++++++++ 2 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 pages/docs/manual/v11.0.0/generalized-algebraic-data-types.mdx diff --git a/data/sidebar_manual_v1100.json b/data/sidebar_manual_v1100.json index 3f3c1a754..28d0b9515 100644 --- a/data/sidebar_manual_v1100.json +++ b/data/sidebar_manual_v1100.json @@ -38,7 +38,8 @@ "Advanced Features": [ "extensible-variant", "scoped-polymorphic-types", - "module-functions" + "module-functions", + "generalized-algebraic-data-types" ], "JavaScript Interop": [ "interop-cheatsheet", diff --git a/pages/docs/manual/v11.0.0/generalized-algebraic-data-types.mdx b/pages/docs/manual/v11.0.0/generalized-algebraic-data-types.mdx new file mode 100644 index 000000000..eb8792be2 --- /dev/null +++ b/pages/docs/manual/v11.0.0/generalized-algebraic-data-types.mdx @@ -0,0 +1,300 @@ +--- +title: "Generalized Algebraic Data Types" +description: "Generalized Algebraic Data Types in ReScript" +canonical: "/docs/manual/v11.0.0/generalized-algebraic-data-types" +--- + +# Generalized Algebraic Data Types + +Generalized Algebraic Data Types (GADTs) are an advanced feature of ReScript's type system. "Generalized" can be somewhat of a misnomer -- what they actually allow you to do is add some extra type-specificity to your variants. Using a GADT, you can give the individual cases of a variant _different_ types. + +For a quick overview of the use cases, reach for GADTs when: + +1. You need to distinguish between different members of a variant at the type level. +2. You want to "hide" type information in a type-safe way, without resorting to casts. +3. You need a function to return a different type depending on its input. + +GADTs usually are overkill, but when you need them, you need them! Understanding them from first principles is difficult, so it is best to explain through some motivating examples. + +## Distinguishing Constructors (Subtyping) + +Suppose a simple variant type that represents the current timezone of a date value. This handles both daylight savings and standard time: + +```res example +type timezone = + | EST // standard time + | EDT // daylight time + | CST // standard time + | CDT // daylight time +// etc... +``` + +Using this variant type, we will end up having functions like this: + +```res example +let convertToDaylight = tz => { + switch tz { + | EST => EDT + | CST => CDT + | EDT | CDT /* or, _ */ => failwith("Invalid timezone provided!") + } +} +``` + +This function is only valid for a subset of our variant type's constructors but we can't handle this in a type-safe way using regular variants. We have to enforce that at runtime -- and moreover the compiler can't help us ensure we are failing only in the invalid cases. We are back to dynamically checking validity like we would in a language without static typing. If you work with a large variant type long enough, you will frequently find yourself writing repetitive catchall `switch` statements like the above, and for little actual benefit. The compiler should be able to help us here. + +Let's see if we can find a way for the compiler to help us with normal variants. We could define another variant type to distinguish the two kinds of timezone. + +```res example +type daylightOrStandard = + | Daylight(timezone) + | Standard(timezone) +``` + +This has a lot of problems. For one, it's cumbersome and redundant. We would now have to pattern-match twice whenever we deal with a timezone that's wrapped up here. The compiler will force us to check whether we are dealing with daylight or standard time, but notice that there's nothing stopping us from providing invalid timezones to these constructors: + +```res example +let invalidTz1 = Daylight(EST) +let invalidTz2 = Standard(EDT) +``` + +Consequently, we still have to write our redundant catchall cases. We could define daylight savings time and standard time as two _separate_ types, and unify those in our `daylightOrStandard` variant. +That could be a passable solution, but what we would really like to do is implement some kind of subtyping relationship. +We have two _kinds_ of timezone. This is where GADTs are handy: + +```res example +type standard +type daylight + +type rec timezone<_> = + | EST: timezone + | EDT: timezone + | CST: timezone + | CDT: timezone +``` + +We define our type with a type parameter. We manually annotate each constructor, providing it with the correct type parameter indicating whether it is standard or daylight. Each constructor is a `timezone`, +but we've added another level of specificity using a type parameter. Constructors are now understood to be `standard` or `daylight` at the _type_ level. Now we can fix our function like this: + +```res example +let convertToDaylight = tz => { + switch tz { + | EST => EDT + | CST => CDT + } +} +``` + +The compiler can infer correctly that this function should only take `timezone` and only output +`timezone`. We don't need to add any redundant catchall cases and the compiler will even error if +we try to return a standard timezone from this function. Actually, this seems like it could be a problem, +we still want to be able to match on all cases of the variant sometimes, and a naive attempt at this will not pass the type checker. A naive example will fail: + +```res example +let convertToDaylight = tz => + switch tz { + | EST => EDT + | CST => CDT + | CDT => CDT + | EDT => EDT + } +``` + +This will complain that `daylight` and `standard` are incompatible. To fix this, we need to explicitly annotate to tell the compiler to accept both: + +```res example +let convertToDaylight : type a. timezone => timezone = // ... +``` + +The syntax `type a.` here defines a _locally abstract type_ which basically tells the compiler that the type parameter a is some specific type, but we don't care what it is. The cost of the extra specificity and safety that +GADTs give us is that the compiler less able to help us with type inference. + +## Varying return type + +Sometimes, a function should have a different return type based on what you give it, and GADTs are how we can do this in a type-safe way. We can implement a generic `add` function[^1] that works on both `int` or `float`: + +[^1]: In ReScript v12, the built-in operators are already generic, but we use them in this example for simplicity. + +```res example +type rec number<_> = Int(int): number | Float(float): number + +let add: + type a. (number, number) => a = + (a, b) => + switch (a, b) { + | (Int(a), Int(b)) => a + b + | (Float(a), Float(b)) => a +. b + } + +let foo = add(Int(1), Int(2)) + +let bar = add(Int(1), Float(2.0)) // the compiler will complain here +``` + +How does this work? The key thing is the function signature for add. The `number` GADT is acting as a _type witness_. We have told the compiler that the type parameter for `number` will be the same as the type we return -- both are set to `a`. So if we provide a `number`, `a` equals `int`, and the function will therefore return an `int`. + +We can also use this to avoid returning `option` unnecessarily. We create an array searching function which either raises an exception, returns an `option`, or provides a `default` value depending on the behavior we ask for.[^2] + +[^2]: This example is adapted from [here](https://dev.realworldocaml.org/gadts.html). + +```res example +module If_not_found = { + type t<_,_> +}module IfNotFound = { + type rec t<_, _> = + | Raise: t<'a, 'a> + | ReturnNone: t<'a, option<'a>> + | DefaultTo('a): t<'a, 'a> +} + +let flexible_find: + type a b. (~f: a => bool, array, IfNotFound.t) => b = + (~f, arr, ifNotFound) => { + open IfNotFound + switch Array.find(arr, f) { + | None => + switch ifNotFound { + | Raise => failwith("No matching item found") + | ReturnNone => None + | DefaultTo(x) => x + } + | Some(x) => + switch ifNotFound { + | ReturnNone => Some(x) + | Raise => x + | DefaultTo(_) => x + } + } + } + +``` + +## Hide and recover Type information Dynamically + +In an advanced case that combines the above techniques, we can use GADTs to selectively hide and recover type information. This helps us create more generic types. +The below example defines a `num` type similar to our above addition example, but this lets us use `int` and `float` arrays +interchangeably, hiding the implementation type rather than exposing it. This is similar to a regular variant. However, it is a tuple including embedding a `numTy` and another value. +`numTy` serves as a type-witness, making it +possible to recover type information that was hidden dynamically. Matching on `numTy` will "reveal" the type of the other value in the pair. We can use this to write a generic sum function over arrays of numbers: + +```res example +type rec numTy<'a> = + | Int: numTy + | Float: numTy +and num = Num(numTy<'a>, 'a): num +and num_array = Narray(numTy<'a>, array<'a>): num_array + +let addInt = (x, y) => x + y +let addFloat = (x, y) => x +. y + +let sum = (Narray(witness, array)) => { + switch witness { + | Int => Num(Int, array->Array.reduce(0, addInt)) + | Float => Num(Float, array->Array.reduce(0., addFloat)) + } +} +``` + +## A Practical Example -- writing bindings: + +Javascript libraries that are highly polymorphic or use inheritance can benefit hugely from GADTs, but they can be useful for bindings even in other cases. The following examples are writing bindings to a simplified +of Node's `Stream` API. + +This API has a method for binding event handlers, `on`. This takes an event and a callback. The callback accepts different parameters +depending on which event we are binding to. A naive implementation might look similar to this, defining a +separate method for each stream event to wrap the unsafe version of `on`. + +```res example +module Stream = { + type t + + @new @module("node:http") external make: unit => t = "stream" + + @send external on : (stream, string, 'a) => unit + let onEnd = (stream, callback: unit=> unit) => stream->on("end", callback) + let onData = (stream, callback: ('a => 'b)) => stream->on("", callback) + // etc. ... +} +``` + +Not only is this quite tedious to write and quite ugly, but we gain very little in return. The function wrappers even add performance overhead, so we are losing on all fronts. If we define subtypes of +Stream like `Readable` or `Writable`, which have all sorts of special interactions with the callback that jeopardize our type-safety, we are going to be in even deeper trouble. + +Instead, we can use the same GADT technique that let us vary return type to vary the input type. +Not only are we able to now just use a single method, but the compiler will guarantee we are always using the correct callback type for the given event. We simply define an event GADT which specifies +the type signature of the callback and pass this instead of a plain string. + +Additionally, we use some type parameters to represent the different types of Streams. + +This example is complex, but it enforces tons of useful rules. The wrong event can never be used +with the wrong callback, but it also will never be used with the wrong kind of stream. The compiler will for example complain if we try to use a `Pipe` event with anything other than a `writable` stream. + +The real magic happens in the signature of `on`. Read it carefully, and then look at the examples and try to +follow how the type variables are getting filled in, write it out on paper what each type variable is equal +to if you need and it will soon become clear. + +```res example + +module Stream = { + type t<'a> + + type writable + type readable + + type buffer = {buffer: ArrayBuffer.t} + + @unboxed + type chunk = + | Str(string) + // Node uses actually its own buffer type, but for the tutorial we are using the stdlib's buffer type. + | Buf(buffer) + + type rec event<_, _> = + // "as" here is setting the runtime representation of our constructor + | @as("pipe") Pipe: event => unit> + | @as("end") End: event<'inputStream, option => unit> + | @as("data") Data: event unit> + + @new @module("node:http") external make: unit => t<'a> = "Stream" + + @send + external on: (t<'inputStream>, event<'inputStream, 'callback>, 'callback) => unit = "on" + +} + +let writer = Stream.Writable.make() +let reader = Stream.Readable.make() +// Types will be correctly inferred for each callback, based on the event parameter provided +writer->Stream.on(Pipe, r => { + Console.log("Piping has started") + + r->Stream.on(Data, chunk => + switch chunk { + | Stream.Str(s) => Console.log(s) + | Stream.Buf(buffer) => Console.log(buffer) + } + ) +}) + +writer->Stream.on(End, _ => Console.log("End reached")) + +``` + +This example is only over a tiny, imaginary subset of Node's Stream API, but it shows a real-life example +where GADTs are all but indispensable. + +## Conclusion + +While GADTs can make your types extra-expressive and provide more safety, with great power comes great +responsibility. Code that uses GADTs can sometimes be too clever for its own good. The type errors you +encounter will be more difficult to understand, and the compiler sometimes requires extra help to properly +type your code. + +However, there are definite situations where GADTs are the _right_ decision +and will _simplify_ your code and help you avoid bugs, even rendering some bugs impossible. The `Stream` example above is a good example where the "simpler" alternative of using regular variants or even strings +would lead to a much more complex and error prone interface. + +Ordinary variants are not necessarily _simple_ therefore, and neither are GADTs necessarily _complex_. +The choice is rather which tool is the right one for the job. When your logic is complex, the highly expressive nature of GADTs can make it simpler to capture that logic. +When your logic is simple, it's best to reach for a simpler tool and avoid the cognitive overhead. +The only way to get good at identifying which tool to use in a given situation is to practice and experiment with both. From 5e728b7f2a8aa66431a6560d585a72ff8758587e Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Fri, 5 Sep 2025 10:22:35 +0200 Subject: [PATCH 3/3] Small fix --- .../docs/manual/v11.0.0/generalized-algebraic-data-types.mdx | 4 +--- .../docs/manual/v12.0.0/generalized-algebraic-data-types.mdx | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/pages/docs/manual/v11.0.0/generalized-algebraic-data-types.mdx b/pages/docs/manual/v11.0.0/generalized-algebraic-data-types.mdx index eb8792be2..b60623817 100644 --- a/pages/docs/manual/v11.0.0/generalized-algebraic-data-types.mdx +++ b/pages/docs/manual/v11.0.0/generalized-algebraic-data-types.mdx @@ -138,9 +138,7 @@ We can also use this to avoid returning `option` unnecessarily. We create an arr [^2]: This example is adapted from [here](https://dev.realworldocaml.org/gadts.html). ```res example -module If_not_found = { - type t<_,_> -}module IfNotFound = { +module IfNotFound = { type rec t<_, _> = | Raise: t<'a, 'a> | ReturnNone: t<'a, option<'a>> diff --git a/pages/docs/manual/v12.0.0/generalized-algebraic-data-types.mdx b/pages/docs/manual/v12.0.0/generalized-algebraic-data-types.mdx index 3982f1f11..42a9df049 100644 --- a/pages/docs/manual/v12.0.0/generalized-algebraic-data-types.mdx +++ b/pages/docs/manual/v12.0.0/generalized-algebraic-data-types.mdx @@ -138,9 +138,7 @@ We can also use this to avoid returning `option` unnecessarily. We create an arr [^2]: This example is adapted from [here](https://dev.realworldocaml.org/gadts.html). ```res example -module If_not_found = { - type t<_,_> -}module IfNotFound = { +module IfNotFound = { type rec t<_, _> = | Raise: t<'a, 'a> | ReturnNone: t<'a, option<'a>>