Functors + function arguments question

Hey guys, trying to build a generic Link react component.
I’ve a Router Functor:

module type Routes = {
  type t;
  let fromUrl: ReasonReactRouter.url => option(t);
  let toString: t => string;
};

module Make = (Routes: Routes) => {
  let fromUrl = Routes.fromUrl;

  let toString = Routes.toString;

  let useRouter = () => ReasonReactRouter.useUrl() |> fromUrl;

  let push = route => {
    route->toString->ReasonReactRouter.push;
  };

  let replace = route => {
    route->toString->ReasonReactRouter.replace;
  };
};

The reason for this, is to allow for shared functionality across multiple projects.

And i’m trying to build a generic Link react component, trying this:

[@react.component]
let make = (~router, ~to_, ~push=?) => {
  let push = push |> Relude.Option.getOrElse(false);
  React.useEffect1(
    () =>
      if (push) {
        router.push(to_);
      } else {
        router.replace(to_);
      },
    (to_, push),
  );
};

But i can’t figure out the type of router.

Is this kind of abstraction even possible in rescript?

1 Like

It’s possible with another functor, I think? Something like:

module Make(Router: Router) = {
  [@react.component]
  let make(~to_, ~push=?) = {
    ...
  };
};

You would need to create an appropriate module type for Router in this case.

1 Like

Use first class modules for your route arg.
Meaning: specify a module type for the returned router module of your functor.
Define the router arg of type (module ModuleTypeOfFunctorReturn) and “unpack” the module inside of your component with: module Router: TypeOfFunctorReturn = (val router);

Mind my example is still in reasonml syntax, since I haven’t come around using fcm’s in rescript yet.

I don’t know any rescript specific source, so here is a link to real world ocaml: https://dev.realworldocaml.org/first-class-modules.html

1 Like

Yeah, the make function can even be in the same module and what I would do is create a MyRouter.res file that includes:

include MakeRouter(MyRoutes)

then you can use <MyRouter to="..." />

1 Like

I replied to this a bit on the Discord chat, but I’ll repost some of that here for posterity.

I’m skeptical that this is a good example for using a functor or a first-class-module. Since all it does is return new functions based on the functions given to it, you could achieve the same thing much easier with a plain-old higher-order-function.

// Return a record with the functions we need: 
let make = (~fromUrl, ~toString) => {
  useRouter: ...,
  push: ...,
  replace: ...,
}

Or you could put it all in a React hook to make the API even simpler:

let useRouter = (~fromUrl, ~toString) => {
  route: fromUrl(ReasonReactRouter.useUrl()),
  push: route => route->toString->ReasonReactRouter.push,
  replace: route => route->toString->ReasonReactRouter.replace,
}

// in your component:
let {route, push, replace} = useRouter(~fromUrl, ~toString)

If you want to use a functor, I’m not sure if the created module would be needed to be passed down your component tree as props. Its only dependencies are the fromUrl and toString functions, which I assume are static across each project.

Here’s a way to use a functor but with a bit easier of an API:

// CommonRouter.res
module Make = (Router: RouterType) => {
  type t = ...
  let fromUrl = ...
  let toString = ...
  let useRouter = ...
  let push = ...
  let replace = ...
}

// ProjectARouter.res
include CommonRouter.Make({
  type t = ...
  let fromUrl = ...
  let toString = ...
})

// ProjectAComponent.res
@react.component
let make = (~to_, ~push=?) => {
  let push = push |> Relude.Option.getOrElse(false);
  React.useEffect1(
    () =>
      if (push) {
        ProjectARouter.push(to_);
      } else {
        ProjectARouter.replace(to_);
      },
    (to_, push),
  );
};

You can still reuse logic across projects but without all of the hassle.

I know this doesn’t actually answer your question about using a module as an argument, but I think there’s probably a better solution to this particular problem.

That being said, I also don’t want to discourage exploring functors and first-class-modules, because you can use them to do some genuinely cool stuff. Just be aware they aren’t always worth the hassle if there’s a simpler solution.

4 Likes