ReasonReact - handling actions with parametized types


#1

Say I have the following state:

type t('a) = {
  foo: string,
  value: 'a
};

type state = {
  one: t(int),
  two: t(float)
};

let state = {
  one: {
    foo: "One int",
    value: 3
  },
  two: {
    foo: "Two float",
    value: 5.3
  }
};

And I have a set of functions that operate on t:

let setFoo = (t, foo) => { ...t, foo };
let setValue = (t, value) => { ...t, value };

I’d like to set up ReasonReact actions that can run a function on any value of type t, rather than have a unique set of actions for each combination. In my real app I have a lot more values of t, so this would quickly grow unwieldly.

My approach is to define a type for each value in state, used to determine which value in state should
be operated on. I can switch on that type to run the desired function on the relevant part of the state:

type stateKey = One | Two;

type action =
 | SetFooOn(stateKey, string);

let handleState = (action, state) => {
  switch (action) {
    | SetFooOn(stateKey, value) => switch (stateKey) {
      | One => { ...state, one: setFoo(state.one, value) }
      | Two => { ...state, two: setFoo(state.two, value) }
    }
  };
};

handleState(SetFooOn(One, "Hello"));
handleState(SetFooOn(Two, "Goodbye"));

That works. But this approach doesn’t work for t.value - the parameterized value.

type actionTwo('a) =
  | SetValueOn(stateKey, 'a);

let handleStateTwo = (action, state) => {
  switch (action) {
    | SetValueOn(stateKey, value) => switch (stateKey) {
      | One => { ...state, one: setValue(state.one, value) }
      | Two => { ...state, two: setValue(state.two, value) }
    }
  };
};

handleStateTwo(SetValueOn(One, 3), state);
handleStateTwo(SetValueOn(Two, 6.23), state); /* doesn't work - compiler wants an int */

Coming from languages without type inference, I would expect the type of “value” could just be ignored and passed through - but of course then the compiler can’t guarantee that stateKey and value are compatible. handleStateTwo(SetValueOn(One, 3.7)) should not be let through as One is associated with t(int).

I’m a bit stumped on how to proceed - whether my issue is solvable, or perhaps I’m going about it in completely the wrong way to begin with?

Thanks for reading.


#2

Here is a different approach to modelling the type state so that the value can be either int or float.

type value = 
  | Int(int) 
  | Float(float);

type state = { 
  foo: string, 
  value
};

type action = 
  | UpdateInt(int)
  | UpdateFloat(float);
  
let handleState = (action, state) =>
  switch (action) {
    | UpdateInt(i) => {...state, value: Int(i)}
    | UpdateFloat(f) => {...state, value: Float(f)}
  };

let intState = { foo: "initial state", value: Int(2) };
let intState' = intState |> handleState(UpdateInt(3));

let floatState = { foo: "initial state", value: Float(1.16)};
let floatState' = floatState |> handleState(UpdateFloat(3.14));

#3

I solved this by inverting it - rather than passing the state key to the action, I pass the action to a state key action. The outer state key action specifies which part of the state to update, the inner action applies the function on the data. I’m not sure if this is the best solution but seems pretty simple and easy to extend with new actions.

type actionTwo('a) =
  | SetValueOn('a);

type action =
  | One(actionTwo(int))
  | Two(actionTwo(float));

let handleStateAction = (action, state) => {
  switch (action) {
    | SetValueOn(value) => setValue(state, value)
  };
};

let handleStateTwo = (action, state) => {
  switch (action) {
    | One(stateAction) => { ...state, one: handleStateAction(state.one, stateAction) }
    | Two(stateAction) => { ...state, two: handleStateAction(state.two, stateAction) }
  };
};

handleStateTwo(One(SetValueOn(3)), state);
handleStateTwo(Two(SetValueOn(6.23)), state);

#4

Look at this post Parameter polymorphic
It looks like what you want to do.