Using generic function return values in functors

Hello all, first of all I’m new to OCaml world so I may be missing most of the needed vocabulary to properly explain what I’m trying to achieve, so to make things easier I made a sketch.sh of what I’m trying to do: Sketch.sh - Interactive ReasonML/OCaml sketchbook

Copying and pasting here for anyone that may not be able to access sketch.sh:

Higher-Order Component using Functors

I’m trying to create a Functor that acts as a Higher-Order Component in ReScript React. Since OCaml community is bigger, the code will be in OCaml.

What am I trying to achieve?

I don’t have a use-case for HoC, I was nerd sniped by a friend and now it became a interesting problem to sink time and brain-effort into. But I do have some constraints that, in my mind, makes it “useable” in real world. A HoC has to:

  1. Be a Functor
  2. When defining a component using it, I should be able to pass as props the HoC props and child props effortlessly. For example: <SuperButton hocProp=true childProp=true />

So, without further ado, let’s begin

Component module type

We need to be able to identify a component in our Functor, so let’s create a module type for it.

module type Component =
  sig
    type props
    type makeProps
    val makeProps: ?key: string -> makeProps
  end

This module signature will not work today in ReScript React because the @react.component ppx only outputs the makeProps and make functions, and not their typings.

I decided to separate the ?key from the rest of the makeProps arguments because all makeProps definitions have a ?key and we want to be able to pass the key down to the child component.

Creating an Example Component

To illustrate our use-case, we’ll create a ExampleComponent that’s as close to what @react.component outputs as possible

module ExampleComponent = struct
  type props = {childProp: string}
  type makeProps = childProp: string -> unit -> props
  
  let makeProps ?key ~childProp () = {childProp}
end

For reference, here’s the true output: syntax/commentAtTop.res.txt at master · rescript-lang/syntax · GitHub

Creating the Functor

Now this is where I don’t know how to proceed. Let’s create the first version I thought of and then I’ll explain why it doesn’t meet my criteria and what I don’t know

module Make (C: Component) = struct
  let makeProps ~hocProp ?key = C.makeProps ?key
end

This interface is the ideal interface (although the result is not ideal), as it allows us to use it as follow

module MakeExampleComponent = Make(ExampleComponent);;
MakeExampleComponent.makeProps ~hocProp:"prop from the Make Functor" ~childProp:"prop from the child Component" ()

See where it fails to meet the criteria? Although I can pass the ~hocProp, I can’t use it to return the child props and the hoc props. The only ways I can think of is to know which Component instance will be passed to the functor, and create a makeProps that receives all arguments from C.makeProps, apply it to C.makeProps getting the return and creating a return value using it and the hoc props. Like the following

module type Component2 =
  sig
    type props
    type makeProps
    val makeProps: ?key: string -> childProp: string -> props
  end

module Make2 (C: Component2) = struct
  type props = {hocProp: string; childProp: C.props}
  let makeProps ~hocProp ?key ~childProp =
    let childProp = C.makeProps ?key ~childProp in
    {
      hocProp;
      childProp;
    }
end

But this defeats the purpose of a Functor, as I’m not being truly generic over any @react.component

Help Wanted

If this also snipes you, try to make the following compile

module Make (C: Component) = struct
  type props = {hocProp: string; childProps: C.props}
  let makeProps ~hocProp ?key : props = C.makeProps ?key
end

The above was the last part of the sketch. I would appreciate any comment about if it’s possible or not to do what I’m trying to do

As far as I understand, the issue start here:

module type Component =
  sig
    type props
    type makeProps
    val makeProps: ?key: string -> makeProps
  end

since the type props is not connected to anything in the module type.
If we restrict the makeProps function to have a well-defined argument and return type:

module type Component = sig
    type arg
    type props
    val makeProps: ?key: string -> arg -> props
  end

it becomes possible and simple to merge together different arguments and return types:


type ('a,'b) hoc_props = { my_prop:'a; child_prop: 'b }
module Make(C: Component) :
  Component
  with type props = (int, C.props) hoc_props
   and type arg = int * C.arg
= struct
  type props = (int, C.props) hoc_props
  type arg = int * C.arg
  let makeProps ? key (hocArg,childArg) =
    { my_prop = hocArg; child_prop = C.makeProps ?key childArg}
end

The part that might not be clear is that, in order to be compatible with the React JSX, it needs to take arguments labeled. So makeProps need to be of the form ?key:.. -> arg1:... -> arg2:.... -> ... -> prop

This is going to be a bit challenging. OCaml doesn’t allow you to manipulate the labels much. Even heterogeneous lists wouldn’t help much here. Maybe a finally tagless encoding can help (see this).

Thanks for the reply! I’m not sure I fully grasp how OCaml deals with the makeProps signature you proposed. I tried to make a module that fits the signature to test it out, but I’m getting a compile error

module Test : Component = struct
  type arg = childProp: string -> unit
  type props = {childProp: string}
  let makeProps ?key ~childProp () = {childProp}
end

The error:

Error: Signature mismatch:
       ...
       Values do not match:
         val makeProps : ?key:'a -> childProp:string -> unit -> props
       is not included in
         val makeProps : ?key:string -> arg -> props

But as @Drup commented, does it work with labeled arguments?

Thanks for the suggestions on Typed Tagless Final! I’ll watch it and see how to apply

Indeed, my proposition cannot work with labels because labels are not first class enough in OCaml to allow composition. In some sense, one would want a form of label polymorphism.

Since that doesn’t exist, the React JSX API is not really composable, at least in term of higher-order components and the lack of useful common signature for components is one of the manifestation of this issue.

I am not sure to have understood how rigid the module type Component is to be compatible with ReScript React, but I think we have a solution if we can “open” the makeProps type, so that the return type of makeProps_cps can be changed by the continuation:

module type Component =
  sig
    type props
    type 'a openMakeProps
    type makeProps = props openMakeProps
    val makeProps_cps : (props -> 'a) -> ?key: string -> 'a openMakeProps
    val makeProps: ?key: string -> makeProps
  end

For instance, the implementation of ExampleComponent becomes:

module ExampleComponent = struct
  type props = {childProp: string}
  type 'a openMakeProps = childProp: string -> unit -> 'a
  type makeProps = childProp: string -> unit -> props

  let makeProps_cps k ?key ~childProp () = k { childProp }

  let makeProps ?key = makeProps_cps Fun.id ?key
end

(Note that the implementation of makeProps given above is generic and suits for every Component: one can imagine a functor which builds a Component from a reduced signature which only defines props, openMakeProps and makeProps_cps.)

Then, we can define the functor Make as follows (I defined the type make_props outside the functor to be able to expose it in the interface by constraining Component).

type 'a make_props = {hocProp: string; childProps: 'a}

module Make (C: Component) : Component with
  type props = C.props make_props and
  type 'a openMakeProps = hocProp:string -> 'a C.openMakeProps
= struct
  type props = C.props make_props
  type 'a openMakeProps = hocProp:string -> 'a C.openMakeProps
  type makeProps = props openMakeProps
  let makeProps_cps k ?key ~hocProp =
    C.makeProps_cps (fun childProps -> k { hocProp; childProps }) ?key
  let makeProps ?key = makeProps_cps Fun.id ?key
end

The test case type-checks as expected:

module MakeExampleComponent = Make(ExampleComponent);;
MakeExampleComponent.makeProps ~hocProp:"prop from the Make Functor" ~childProp:"prop from the child Component" ()
1 Like

This example makes me realize that warning 16 can have false alarms when the result type is abstract. That is the case in the example above for makeProps_cps and makeProps, where the result type is an abstract type coming from the parameter of a functor, but that can be the case as well for any abstract type, because there can exist some type equality carried by a GADT that makes this result type equal to an arrow type. Those false alarms predate https://github.com/ocaml/ocaml/pull/9783 and seem inevitable because silencing the warning as soon as the result type is abstract will miss important cases where we want such a warning.

I don’t know what can be done for this problem. It seems to me that there is little point to write an issue since I believe it can’t be fixed, but perhaps we may document somewhere that this warning may be triggered for some classes of legitimate code, that can be annotated with [@@ocaml.warning "-unerasable-optional-argument"].