From 7c58c08fccff94d1ff267d393afb88ddf3dace6a Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Thu, 2 May 2024 07:58:57 -0400 Subject: [PATCH 1/8] docs: replace usage of functor with module functions --- pages/docs/manual/latest/module.mdx | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pages/docs/manual/latest/module.mdx b/pages/docs/manual/latest/module.mdx index 3dbf4dfe1..018785b5d 100644 --- a/pages/docs/manual/latest/module.mdx +++ b/pages/docs/manual/latest/module.mdx @@ -413,22 +413,22 @@ type state = int let render: string => string ``` -## Module Functions (functors) +## Module Functions 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". +functions. Instead, we pass them to special functions called module functions. -The syntax for defining and using functors is very much like the syntax +The syntax for defining and using module functions is very much like the syntax for defining and using regular functions. The primary differences are: -- Functors use the `module` keyword instead of `let`. -- Functors take modules as arguments and return a module. -- Functors *require* annotating arguments. -- Functors must start with a capital letter (just like modules/signatures). +- Module functions use the `module` keyword instead of `let`. +- Module functions take modules as arguments and return a module. +- Module functions *require* annotating arguments. +- Module functions must start with a capital letter (just like modules/signatures). -Here's an example `MakeSet` functor, that takes in a module of the type +Here's an example `MakeSet` module function, that takes in a module of the type `Comparable` and returns a new set that can contain such comparable items. @@ -482,7 +482,7 @@ function MakeSet(Item) { -Functors can be applied using function application syntax. In this case, we're +Module functions can be applied using function application syntax. In this case, we're creating a set, whose items are pairs of integers. @@ -525,12 +525,12 @@ var SetOfIntPairs = { ### Module functions types -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 +Like with module types, module function types also act to constrain and hide what we may +assume about module functions. The syntax for module function 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 +modules the module functions 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! +a module function signature, we can hide the underlying data structure! @@ -566,4 +566,4 @@ Please note that modules with an exotic filename will not be accessible from oth ## Tips & Tricks -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. +Modules and module functions 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. From 150f7b72d8afeb628e9db88df7ef9203efc39e36 Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Thu, 2 May 2024 08:11:00 -0400 Subject: [PATCH 2/8] create module-functions page --- data/sidebar_manual_latest.json | 3 ++- pages/docs/manual/latest/module-functions.mdx | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 pages/docs/manual/latest/module-functions.mdx diff --git a/data/sidebar_manual_latest.json b/data/sidebar_manual_latest.json index d783a4eb3..6e6c86255 100644 --- a/data/sidebar_manual_latest.json +++ b/data/sidebar_manual_latest.json @@ -37,7 +37,8 @@ ], "Advanced Features": [ "extensible-variant", - "scoped-polymorphic-types" + "scoped-polymorphic-types", + "module-functions" ], "JavaScript Interop": [ "interop-cheatsheet", diff --git a/pages/docs/manual/latest/module-functions.mdx b/pages/docs/manual/latest/module-functions.mdx new file mode 100644 index 000000000..b72d376d2 --- /dev/null +++ b/pages/docs/manual/latest/module-functions.mdx @@ -0,0 +1,7 @@ +--- +title: "Module Functions" +description: "Module Functions in ReScript" +canonical: "/docs/manual/latest/module-functions" +--- + +# Module Functions \ No newline at end of file From f65793363f509c45ac0f0fbeb14f5b710526a3b6 Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Thu, 2 May 2024 11:17:48 -0400 Subject: [PATCH 3/8] add example for module functions --- pages/docs/manual/latest/module-functions.mdx | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/pages/docs/manual/latest/module-functions.mdx b/pages/docs/manual/latest/module-functions.mdx index b72d376d2..010cc066a 100644 --- a/pages/docs/manual/latest/module-functions.mdx +++ b/pages/docs/manual/latest/module-functions.mdx @@ -4,4 +4,37 @@ description: "Module Functions in ReScript" canonical: "/docs/manual/latest/module-functions" --- -# Module Functions \ No newline at end of file +# Module Functions + +```rescript +// next.res - bindings for Next +module type Params = { + type t +} + +module MakeNavigation = (Params: Params) => { + @module("next/navigation") + external useNavigation: unit => Params.t = "useNavigation" +} + +// Your React component +// component.res +module Params = { + type t = { + tag: string, + item: string, + } +} + +module Navigation = Next.MakeNavigation(Params) + +@react.component +let make = () => { + let params = Navigation.useNavigation() +
+

{React.string("Tag: " ++ params.tag)}

+

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

+
+} +``` +import { comma } from "postcss/lib/list" From ec753ff3b7442be2bbaf5db731e73ab3e9af6f20 Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Thu, 2 May 2024 11:26:51 -0400 Subject: [PATCH 4/8] docs - add js output to module function example --- pages/docs/manual/latest/module-functions.mdx | 98 ++++++++++++++----- 1 file changed, 75 insertions(+), 23 deletions(-) diff --git a/pages/docs/manual/latest/module-functions.mdx b/pages/docs/manual/latest/module-functions.mdx index 010cc066a..ad803e4b2 100644 --- a/pages/docs/manual/latest/module-functions.mdx +++ b/pages/docs/manual/latest/module-functions.mdx @@ -6,35 +6,87 @@ canonical: "/docs/manual/latest/module-functions" # Module Functions -```rescript -// next.res - bindings for Next -module type Params = { - type t -} + +```res example +module Next = { + // define a module type to use a parameter for out module function + module type Params = { + type t + } -module MakeNavigation = (Params: Params) => { - @module("next/navigation") - external useNavigation: unit => Params.t = "useNavigation" + // define our module function + module MakeNavigation = (Params: Params) => { + @module("next/navigation") + external useNavigation: unit => Params.t = "useNavigation" + /* You can use values from the function parameter, such as Params.t */ + } } -// Your React component -// component.res -module Params = { - type t = { - tag: string, - item: string, +module Component: { + @react.component + let make: unit => Jsx.element +} = { + // Create a module that matches the module type expected by Next.MakeNavigation + module Params = { + type t = { + tag: string, + item: string, + } } + + // Create a new module using the Params module we created and the Next.MakeNavigation module function + module Navigation = Next.MakeNavigation(Params) + + @react.component + let make = () => { + // Use the functions, values, or types created by the module function + let params = Navigation.useNavigation() +
+

+ {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 MakeNavigation(Params) { + return {}; } -module Navigation = Next.MakeNavigation(Params) +var Next = { + MakeNavigation: MakeNavigation +}; -@react.component -let make = () => { - let params = Navigation.useNavigation() -
-

{React.string("Tag: " ++ params.tag)}

-

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

-
+function Playground$Component(props) { + var params = $$Navigation.useNavigation(); + 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 */ + ``` -import { comma } from "postcss/lib/list" + \ No newline at end of file From f3d715048fe35000d4a6d3bff9e7aba000b079f3 Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Thu, 2 May 2024 11:57:55 -0400 Subject: [PATCH 5/8] add another example --- pages/docs/manual/latest/module-functions.mdx | 68 +++++++++++++++++-- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/pages/docs/manual/latest/module-functions.mdx b/pages/docs/manual/latest/module-functions.mdx index ad803e4b2..92e606e18 100644 --- a/pages/docs/manual/latest/module-functions.mdx +++ b/pages/docs/manual/latest/module-functions.mdx @@ -6,16 +6,21 @@ canonical: "/docs/manual/latest/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 created abstractions and reusable code that might not be possible with functions, or might have a runtime cost if done with functions. + +Next.js has a `useNavigation` hook that returns an unknown type, +and it's up to the developer to add in a type annotation to define the type of the parameters returned by the hook. +```TS +const params = useParams<{ tag: string; item: string }>() +``` + +Instead of having to add the type annotation every time you use the hook, you can create a module function that will return a typed response for the `useNavigation` hook. ```res example module Next = { - // define a module type to use a parameter for out module function - module type Params = { - type t - } - // define our module function - module MakeNavigation = (Params: Params) => { + module MakeNavigation = (Params: { type t }) => { @module("next/navigation") external useNavigation: unit => Params.t = "useNavigation" /* You can use values from the function parameter, such as Params.t */ @@ -88,5 +93,56 @@ export { } /* next/navigation Not a pure module */ +``` + + +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 `useEnv` hook to load in environment variables found in `import.meta.env`. +```res +@val external env: 'a = "import.meta.env" + +let useEnv = () => { + 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 when they use the hook. +```res +type t = {"LOG_LEVEL": string} + +let values: t = useEnv() +``` +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 `useEnv` hook that has a typed response. +```res +module MakeEnv = ( + E: { + type t + }, +) => { + @val external env: E.t = "import.meta.env" + + let useEnv = () => { + env + } +} +``` +And now consumers of our library can define the types and create a custom version of the hook just 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.useEnv() +``` +```js +var Env = { + useEnv: useEnv +}; + +var values = import.meta.env; ``` \ No newline at end of file From 8f7072f5696a36389cfea2a867f3403cb29af565 Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Thu, 2 May 2024 16:39:22 -0400 Subject: [PATCH 6/8] more module function examples --- pages/docs/manual/latest/module-functions.mdx | 163 +++++++++++++++++- 1 file changed, 162 insertions(+), 1 deletion(-) diff --git a/pages/docs/manual/latest/module-functions.mdx b/pages/docs/manual/latest/module-functions.mdx index 92e606e18..883cd9de0 100644 --- a/pages/docs/manual/latest/module-functions.mdx +++ b/pages/docs/manual/latest/module-functions.mdx @@ -9,6 +9,7 @@ canonical: "/docs/manual/latest/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 created abstractions and reusable code that might not be possible with functions, or might have a runtime cost if done with functions. +## Quick example Next.js has a `useNavigation` hook that returns an unknown type, and it's up to the developer to add in a type annotation to define the type of the parameters returned by the hook. ```TS @@ -96,6 +97,7 @@ export { ``` +## 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 `useEnv` hook to load in environment variables found in `import.meta.env`. ```res @@ -145,4 +147,163 @@ var Env = { var values = import.meta.env; ``` - \ No newline at end of file + + +## 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 a 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 */ +``` + \ No newline at end of file From f61d5ddbabef1bfcddb69e30aa1de370c9e3c13c Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Mon, 13 May 2024 10:39:13 -0400 Subject: [PATCH 7/8] one less react example --- pages/docs/manual/latest/module-functions.mdx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pages/docs/manual/latest/module-functions.mdx b/pages/docs/manual/latest/module-functions.mdx index 883cd9de0..df26ddcb6 100644 --- a/pages/docs/manual/latest/module-functions.mdx +++ b/pages/docs/manual/latest/module-functions.mdx @@ -99,23 +99,23 @@ export { ## 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 `useEnv` hook to load in environment variables found in `import.meta.env`. +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 useEnv = () => { +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 when they use the hook. +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 when they use the function. ```res type t = {"LOG_LEVEL": string} -let values: t = useEnv() +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 `useEnv` hook that has a typed response. +We can instead create a module function that can return a module that has contains a `getEnv` hook that has a typed response. ```res module MakeEnv = ( E: { @@ -124,7 +124,7 @@ module MakeEnv = ( ) => { @val external env: E.t = "import.meta.env" - let useEnv = () => { + let getEnv = () => { env } } @@ -138,11 +138,11 @@ module Env = MakeEnv({ type t = {"LOG_LEVEL": string} }) -let values = Env.useEnv() +let values = Env.getEnv() ``` ```js var Env = { - useEnv: useEnv + getEnv: getEnv }; var values = import.meta.env; From 12193fcfc67f622dc8b72ddc7d0d9909db195953 Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Tue, 26 Nov 2024 12:23:52 -0300 Subject: [PATCH 8/8] editing pass --- pages/docs/manual/latest/module-functions.mdx | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/pages/docs/manual/latest/module-functions.mdx b/pages/docs/manual/latest/module-functions.mdx index df26ddcb6..04c6ff458 100644 --- a/pages/docs/manual/latest/module-functions.mdx +++ b/pages/docs/manual/latest/module-functions.mdx @@ -7,23 +7,25 @@ canonical: "/docs/manual/latest/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 created abstractions and reusable code that might not be possible with functions, or might have a runtime cost if done with functions. +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 `useNavigation` hook that returns an unknown type, -and it's up to the developer to add in a type annotation to define the type of the parameters returned by the hook. +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 }>() ``` -Instead of having to add the type annotation every time you use the hook, you can create a module function that will return a typed response for the `useNavigation` hook. +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 MakeNavigation = (Params: { type t }) => { + module MakeParams = (Params: { type t }) => { @module("next/navigation") - external useNavigation: unit => Params.t = "useNavigation" + external useParams: unit => Params.t = "useParams" /* You can use values from the function parameter, such as Params.t */ } } @@ -32,21 +34,21 @@ module Component: { @react.component let make: unit => Jsx.element } = { - // Create a module that matches the module type expected by Next.MakeNavigation - module Params = { + // 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.MakeNavigation module function - module Navigation = Next.MakeNavigation(Params) + // 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 = Navigation.useNavigation() + let params = Params.useParams()

{React.string("Tag: " ++ params.tag /* params is fully typed! */)} @@ -62,16 +64,16 @@ module Component: { import * as $$Navigation from "next/navigation"; import * as JsxRuntime from "react/jsx-runtime"; -function MakeNavigation(Params) { +function MakeParams(Params) { return {}; } var Next = { - MakeNavigation: MakeNavigation + MakeParams: MakeParams }; function Playground$Component(props) { - var params = $$Navigation.useNavigation(); + var params = $$Navigation.useParams(); return JsxRuntime.jsxs("div", { children: [ JsxRuntime.jsx("p", { @@ -107,7 +109,7 @@ 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 when they use the function. +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} @@ -115,7 +117,7 @@ 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` hook that has a typed response. +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: { @@ -129,7 +131,7 @@ module MakeEnv = ( } } ``` -And now consumers of our library can define the types and create a custom version of the hook just for their application. +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. @@ -249,7 +251,7 @@ function Person$1(props) { ## Dependency injection Module functions can be used for dependency injection. -Here's an example of injecting in a some config values into a set of functions to access a database. +Here's an example of injecting in some config values into a set of functions to access a database. ```res module type DbConfig = {