Need some advice regarding types


#1

I’ve run into a situation with a project that I’m working on. I think it’s analogous to the following:

exception MyError;

type mytype =
  | Bar(int)
  | Baz(string);

let myfun = returnType => {
  switch (returnType) {
  | Bar(i) => Bar(i + 1)
  | Baz(s) => Baz(s ++ " world")
  };
};
let a = myfun(Bar(3)); // type of a is mytype
let b = myfun(Baz("hello")); // type of b is mytype

/* what if I want to add +2 to a? */
/* I know that it is a Bar type, because */
/* I passed in a Bar type in my "let a" statement. */
/* I have to do something like: */
switch (a) {
| Bar(i) => Bar(i + 2)
| _ => raise(MyError)
};

Is there a way to prove to the type system that the type of variable “a” is not just mytype, but mytype.Bar ?


#2

If I understand correctly, Bar is not a type at all, mytype is. Also, you may “know” things today, but can you prove that it will stay that way? ML types are all about provability. E.g., if the value is of type mytype, the code that works with it should cover all the possible mytype values.

There is a bunch of possibilities, like creating mapping functions (say, mapBar and mapBaz), or operating on an integer and then wrapping it in your mytype container. But first of all, what are you actually trying to model? Your example looks like an XY problem.


#3

Thanks for the sanity check. I think that I was overthinking this.

I was wondering if variant types might not be the right thing to use here, but it sounds like it is. Your suggestion to write a mapping functions is the right way to go.

You asked what I’m trying to model. It’s a Reason React app, featuring a series of questions. Each question has some text, and an input type. The input type is either text field or a checkbox. The user interacts with it and the result is either text or a bool, depending.

type userInput =
  | Bool(option(bool))
  | Text(option(string));

type question = {
  text: string,
  userInput: userInput
};

let makeQuestion = (text, userInput) => {
  {text, userInput};
};

let makeInputField = (question: question) => {
  switch (question.userInput) {
  | Text(initialVal) =>
    let htmlElement = .../* text input box */;
    let inputFieldValue = /* text which user typed in */;
    (htmlElement, inputFieldValue);
  | Bool(_) =>
    let htmlElement = ... /* checkbox */ ;
    let inputFieldValue = /* the contents of the checkbox, true/false */;
    (htmlElement, inputFieldValue);
  };
};

When I call makeInputField, I get back the HTML element to render, and a inputFieldValue of type userInput, which is either Bool(option(bool)) or Text(option(string)), with the contents of the user’s response after they interact with that input.

I wanted an easy way to get the wrapped value, which is a bool or a string. I can just write a mapping function with some reasonable false-ish values. Something like this:

let getUserInputStr = (inputFieldValue) => {
  switch(inputFieldValue){
  | Text(Some(str)) => str
  | _ => ""
  }
}

let getUserInputBool = (inputFieldValue) => {
  switch(inputFieldValue){
  | Bool(Some(b)) => b
  | _ => false
  }
}

#4

Hmm… still not sure what are you trying to model exactly. Like, why Text(option(string)) and not, say, Text(string)? And how do you use makeInputField? Is htmlElement a React element or something else?


#5

Text(option(string)) because I want None to represent an input that hasn’t been touched. So for example a checkbox can have state Some(true), Some(false), or None, where None means it hasn’t been interacted with. An input can have Some(“text”), Some(""), or None. I might change this, but it might be helpful for validating form fields. Not sure yet.

Yes this is a React app, and the code-snippet above doesn’t make that very clear. htmlElement is a react element, and inputFieldValue is value that you get from let (value, setValue) = useState(()=> initialValue)


#6

full code available here if you’re curious: https://github.com/rashkov/questionnaire-reason/blob/master/src/Questionnaire.re

It won’t compile until I use the map function that you suggested


#7

The type will be tighter and the problem will go away if the data that the user types in is not UserInput(x), but just x.

That is, in https://github.com/rashkov/questionnaire-reason/blob/master/src/Questionnaire.re#L26

switch (question.userInput) {
  | Text(initialVal) =>
    let (value: userInput, setValue) = RR.useState(() => Text(initialVal));

Here, the state is Text(string), but since we already know that question.userInput is Text, the state can just be string.

Same with the other input types.

The principle here is to “make invalid state impossible”. By allowing the answer state to be of type userInput, we’re making it possible for it to be inconsistent with the question’s userInput. By eliminating that and using the inner type (since we already know it is of Text or Bool etc within the switch statement), you no longer need to unwrap anything.