Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parametric state, thought I could get this to type check but doesn't #148

Closed
fxfactorial opened this issue Dec 28, 2017 · 5 comments
Closed

Comments

@fxfactorial
Copy link

fxfactorial commented Dec 28, 2017

Ping @cristianoc

I am trying to get this to type check but while it passes merlin, the compiler is not so happy and throws a type error

  Here's the original error message
  This expression's type contains type variables that can't be generalized:
  ReasonReact.componentSpec
  (input(Js.t({_..  })),  ReasonReact.stateless,
    ReasonReact.noRetainedProps,  ReasonReact.noRetainedProps,  action)

And I thought it might be because your own componentSpec definition of state doesn't not include a type variable but I thought I was getting around this with the local abstract type.

  1. Am I conceptually writing gibberish?
  2. How to parameterize over state? I will try to cheat with a functor now..

full code:

type input('payload) = {repos: list((Js.t({..}) as 'payload))};
type action =
  | Pull_data;

type thing = {. "stars": int};
let s = ReasonReact.stringToElement;

let stars_component = ReasonReact.reducerComponent("Stars");

let make =
    (type f, children)
    : ReasonReact.componentSpec(
        input(Js.t(({..} as 'f))),
        input(Js.t(({..} as 'f))),
        ReasonReact.noRetainedProps,
        ReasonReact.noRetainedProps,
        action
      ) => {
  ...stars_component,
  initialState: fun () => ({repos: []}: input(Js.t(({..} as 'f)))),
  reducer: (action, _) =>
    switch action {
    | Pull_data => ReasonReact.NoUpdate
    },
  render: (self) => {
    let inner =
      self.state.repos
      |> List.map((i) => <p> (s(i##color)) </p>)
      |> Array.of_list
      |> ReasonReact.arrayToElement;
    <div style onClick=(self.reduce((_) => Pull_data))> inner </div>
  }
};

EDIT:

Also trying this and some variant of it but getting syntax errors on the constraint:

module type Payload = {type t; let init_state: unit => t;};

module MakeGithub = (DataSource: Payload) => {
  type action =
    | Tap;
  type state = {
    name: string,
    data: DataSource.t
  }
  constraint DataSource.t = Js.t({.., owner: string, stars: int});

  let github = ReasonReact.reducerComponent("Impl");
  let make = (children) => {
    ...github,
    initialState: DataSource.init_state,
    reducer: (act: action, _) =>
      switch act {
      | Tap => ReasonReact.NoUpdate
      },
    render: (self) => {
      let g: state = self.state;
      <div> (ReasonReact.stringToElement("hi")) </div>
    }
  };
};
module Made =
  MakeGithub(
    {
      type t = {. "owner": string, "stars": int};
      let init_state = () => {"owner": "hello", "stars": 10};
    }
  );
@cristianoc
Copy link
Contributor

@fxfactorial likely merlin does not show the type error while the error is there. I've seen that before.

When a component is created, componentSpec should not contain type variables, so trying direct parametric state will give type errors.

Using functors, perhaps on simplified version of the example, should give code that compiles.
Alternatively, the creation of the component can be delayed using a function, so that when the function is applied, there are no type variables left.

@jordwalke
Copy link
Member

jordwalke commented Dec 30, 2017

Some of the underlying reasons why state having type parameters is incompatible with React's model:

  • Suppose you have a Component with state(int) and another Component with state type state(string). These two components cannot be reconciled together in an update - when they are at the same position in the tree. One must first be destroyed, and then the other created in its place. But type parameters are fully erased at compile time! So there's no way to detect at runtime that they are components whose state types are parameterized differently. The solution is to do anything that would make them be two totally different "components" types as @cristianoc mentioned. Anytime you "make a new component" you are actually creating a new runtime value that can be compared at runtime to another component at the same position in the tree, when performing updates.

@jordwalke
Copy link
Member

jordwalke commented Dec 30, 2017

In your case the polymorphism is coming from OCaml's row polymorphic objects. Objects do also have a way to coerce away the polymorphic aspect using :>. Here's a simplified version that might type check (if you imagine the div's event returned an object with #color field), while keeping your components not-polymorphic because the polymorphism is coerced away before making its way into the list. The only thing that might now work in this case, is that self.reduce might not be able to handle lambdas that are themselves polymorphic - I can't recall. If not, a special reducer form could be added that can. The gist of what I'm recommending is that you might be able to coerce away the polymorphism at the right time to keep your components monomorphic.

  type repo = {. color: string};
  type state = {repos: list(repo)};
  type action =
    | NewItem(repo);
  type thing = {. "stars": int};
  let s = ReasonReact.stringToElement;
  let stars_component = ReasonReact.reducerComponent("Stars");
  let make = children => {
    ...stars_component,
    initialState: () => {repos: []},
    reducer: (action, state) =>
      switch action {
      | NewItem(repo) => ReasonReact.Update({repos: [repo, ...state.repos]})
      },
    render: self => {
      let inner =
        self.state.repos
        |> List.map(i => <p> (s(i#color)) </p>)
        |> Array.of_list
        |> ReasonReact.arrayToElement;
      <div onClick=(self.reduce(repo => NewItem((repo :> repo))))>
        (s("asdf"))
      </div>;
    }
  };

@fxfactorial
Copy link
Author

Thanks!

@cristianoc yes, I was thinking that using functor could delay creation, say create the module in a function. What is the reasoning behind the typing as it is? Jordan's intuitive React explanation makes sense, but wondering if there is also an type system explanation as well.

@jordwalke Can props do this?

@peterpme
Copy link
Collaborator

For the sake of cleaning up the repo (and given how old this issue is) I'm going to close this out.

Please re-open if this is still relevant. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants