Trying to wrap lenses-ppx: "variables that cannot be generalized"

I’m trying to make a hook that wraps the lenses-ppx library.

This hook compiles

module type Lenses = {
  type field<'a>
  type state
  let set: (state, field<'a>, 'a) => state
  let get: (state, field<'a>) => 'a
}

module Make = (Lenses: Lenses) => {
  type lensApi<'a> = {
    state: Lenses.state,
    set: (Lenses.field<'a>, 'a) => unit,
    get: Lenses.field<'a> => 'a,
  }

  let use = (state: Lenses.state) => {
    let (state, setState) = React.useState(_ => state)
    let get = (state, field) => state->Lenses.get(field)
    let set = (state, field, value) => state->Lenses.set(field, value)
    let setState = (field, value) => setState(state => set(state, field, value))

    {
      state: state,
      get: get(state),
      set: setState,
    }
  }
}

But I can’t make a context out of the same api

module type Lenses = {
  type field<'a>
  type state
  let set: (state, field<'a>, 'a) => state
  let get: (state, field<'a>) => 'a
}

module Make = (Lenses: Lenses) => {
  type lensApi<'a> = {
    state: Lenses.state,
    set: (Lenses.field<'a>, 'a) => unit,
    get: Lenses.field<'a> => 'a,
  }

  let use = (state: Lenses.state): lensApi<'a> => {
    let (state, setState) = React.useState(_ => state)
    let get = (state, field) => state->Lenses.get(field)
    let set = (state, field, value) => state->Lenses.set(field, value)
    let setState = (field, value) => setState(state => set(state, field, value))

    {
      state: state,
      get: get(state),
      set: setState,
    }
  }

  type context<'a> = option<lensApi<'a>>

  let initialContext = None
  let context = React.createContext(initialContext)

  exception LensHookProviderNotFound

  let use = () => {
    let fields = React.useContext(context)
    try {
      fields->Option.getExn
    } catch {
    | _ => raise(LensHookProviderNotFound)
    }
  }

  module Provider = {
    // https://forum.rescript-lang.org/t/how-to-use-react-context-in-rescript/897/5?u=tdfairbrother
    include React.Context
    let make = React.Context.provider(context)
  }

  @react.component
  let make = (~state, ~children) => {
    let api = use(state)
    <Provider value=Some(api)> {children} </Provider>
  }
}

I get the following error

The type of this module contains type variables that cannot be generalized:
  (Lenses: Lenses) => {
  type lensApi<'a> = {
    state: Lenses.state,
    set: (Lenses.field<'a>, 'a) => unit,
    get: Lenses.field<'a> => 'a,
  }
  type context<'a> = option<lensApi<'a>>
  let initialContext: option<'a>
  let context: React.Context.t<option<'_weak1>>
  type exn +=  LensHookProviderNotFound
  let use: unit => '_weak1
  module Provider: {
    type t<'props> = React.Context.t<'props>
    external makeProps: (~value: 'props, ~children: React.element, unit) => {
      "children": React.element,
      "value": 'props,
    } =
      "" "#rescript-external"
    external provider: t<'props> => React.component<
      {"children": React.element, "value": 'props},
    > =
      "Provider" "#rescript-external"
    let make: React.component<
      {"children": React.element, "value": option<'_weak1>},
    >
  }
  external makeProps: (
    ~state: 'state,
    ~children: 'children,
    ~?key: string,
    unit,
  ) => {"children": 'children, "state": 'state} =
    "" "#rescript-external"
  let make: {"children": React.element, "state": unit} => React.element
}

  This happens when the type system senses there's a mutation/side-effect,
  in combination with a polymorphic value.
  Using or annotating that value usually solves it.

I know I can solve it using a new GADT type.
Which means changing the api.

  type rec anything =
    | Any(Lenses.field<'a>, 'a): anything
    | State(Lenses.state): anything

  type lensApi = {
    state: Lenses.state,
    // set(Any(SomePolymorphicField, #HelloWorld))
    // set(State(Lenses.state))
    set: anything => unit,
  }

Is there any way I can annotate the first api without forcing consumers to use a new variant?

You have weak type variables. E.g.:

  let context: React.Context.t<option<'_weak1>>

To address weak type variables you need to generally do one of the following:

If it fits your desired API (ie you only need the make function to be visible), the last option can be a simple fix. Something like this:

module Make2 = (Lenses: Lenses): {
  @react.component
  let make: ... => React.element
} => {
...
}
1 Like

Thank you for taking the time to respond. I’ll take a look at the links you posted. I’m not sure I can get away with hiding the weak type.

For a bit of background. I’ve been looking at tidying up the tedious task of writing bespoke setters and getters on record types for form inputs. I’ve looked at Reform and Reschema’s code bases for inspiration.

Something like this.

Lens.res

module type Lenses = {
  type field<'a>
  type state
  let set: (state, field<'a>, 'a) => state
  let get: (state, field<'a>) => 'a
}

module Make = (Lenses: Lenses) => {
  type setState = (Lenses.state => Lenses.state) => unit
  type state = Lenses.state
  type api = {state: state, setState: setState}
  type context = option<api>

  let initialContext: context = None
  let context = React.createContext(initialContext)

  module Provider = {
    // https://forum.rescript-lang.org/t/how-to-use-react-context-in-rescript/897/5?u=tdfairbrother
    include React.Context
    let make = React.Context.provider(context)
  }

  module StateProvider = {
    @react.component
    let make = (~state, ~setState, ~children) => {
      <Provider value=Some({state: state, setState: setState})> {children} </Provider>
    }
  }

  // state consumer
  exception LensStateProviderNotFound

  let use = () => {
    let c = React.useContext(context)
    try {
      c->Option.getExn
    } catch {
    | _ => raise(LensStateProviderNotFound)
    }
  }

  type useStateApi<'a> = {
    state: Lenses.state,
    get: Lenses.field<'a> => 'a,
    set: (Lenses.field<'a>, 'a) => unit,
    handleInput: (field, ev) => set(field, ReactEvent.Form.target(ev)["value"]),
  }

  let useState = () => {
    let {state, setState} = use()
    let set = (field, value) => setState(state => state->Lenses.set(field, value))

    (
      {
        state: state,
        get: field => state->Lenses.get(field),
        set: set,
        handleInput: (field, ev) => set(field, FormUtils.value(ev)),
      }: useStateApi<'a>
    )
  }

  // control
  module Input = {
    @react.component
    let make = (~field, ~className=?, ~placeholder=?, ~disabled=?) => {
      let {get, handleInput} = useState()
      <input
        type_="text"
        value={get(field)}
        onChange={handleInput(field)}
        ?className
        ?placeholder
        ?disabled
      />
    }
  }
}

Form.res

module Lenses = %lenses(
  type state = {
    first: string,
    last: string,
  }
)

module L = Lens.Make(Lenses)

open Lenses

@react.component
let make = () => {
  let initialState: state = {
    first: "",
    last: "",
  }
  let (state, setState) = React.useState(_ => initialState)
  <L.StateProvider state setState> <L.Input field=First /> <L.Input field=Last /> </L.StateProvider>
}

the set and get don’t work as I expected.

  type useStateApi<'a> = {
    state: Lenses.state,
    get: Lenses.field<'a> => 'a,
    set: (Lenses.field<'a>, 'a) => unit,
    handleInput: (field, ev) => set(field, ReactEvent.Form.target(ev)["value"]),
  }

I was able to use it on a bool type, but then it wouldn’t let me use it for a different type (array of strings).

I ended up dropping the get and set functions and exposing the setState directly. The consumers have access to the Leneses module if they need to deal with bespoke types.

First question, if lenses-ppx is giving you want you want, is there a reason you want to drop using it? If your goal is to eliminate your reliance on ppx, okay, then I get that, but if not, why not use the ppx? Second thing, it would be easier to help you out if you could give example that compile, then provide the rescript playground link (or even better a smaller example).

Okay, looking at your code again, would it be acceptable for you to thread the context as an argument through function calls rather than setting it to a default value of None in your functor? (that is where your weak type comes in to play)

First question, if lenses-ppx is giving you want you want, is there a reason you want to drop using it? If your goal is to eliminate your reliance on ppx, okay, then I get that, but if not, why not use the ppx? Second thing, it would be easier to help you out if you could give example that compile, then provide the rescript playground link (or even better a smaller example).

I think you realised that I’m still using the ppx :smile:

Okay, looking at your code again, would it be acceptable for you to thread the context as an argument through function calls rather than setting it to a default value of None in your functor? (that is where your weak type comes in to play)

I don’t think it’s possible. The createContext and Provider.make function create a React component that you can use as a provider. It might be possible, but I don’t know how.

I’ve made some improvements today. I’ll post something once I settle on the api. The playground is a good idea!