Implicit type conversion?

I totally get this whole thing can be surprising to those who are not familiar with HM type systems. There’s actually a pretty cool page that shows how HM type inference works (beware, it doesn’t seem to work with Firefox unfortunately), I can only recommend to take a look at it, it helped me quite a lot.

I might be wrong but 'a is actually not an abstract type, an abstract type is a type you have no information about. For example:

module UUID: {
  type t
  let toString: t => string
  let fromString: string => option<t>
} = {
  type t = string
  let toString = uuid => uuid
  let fromString = s =>
    switch isValidUUID(s) {
    | true => Some(s)
    | false => None
    }
}

let uuidString = "5fb06087-eaff-4d91-86da-acf91e001b46"
Console.log(`this is my UUID: ${uuidString}`)
let uuid = UUID.fromString(uuidString)->Option.getExn
Console.log(`this is a valid UUID: ${uuid}`)
// Error: This has type: UUID.t       ↑
//  But this function argument is expecting: string

Outside of the UUID module, the compiler has no way to know UUID.t is actually a string, UUID.t is an abstract type.

But in our case, 'a is not an abstract type, it’s just a type variable, waiting to be unified with a concrete type, so if there’s no information about its type, the compiler will wait until it’s used to infer its concrete type. You could actually manually add a type signature and it would error as a type mismatch:

let x: string = ReactEvent.Form.target(evt)["value"] 
setValue(_ => x) // 2
//            ↑ This has type: string
//  But it's expected to have type: int
//  You can convert string to int with Belt.Int.fromString.

This binding uses {..} because it’s indeed highly polymorphic and can’t really be made type-safe statically, but the good news is this kind of corner cases is extremely uncommon in ReScript, so don’t get too alarmed about it :slight_smile:

I hope my message can help you (and other newcomers) understand the issue, and don’t worry, I really don’t think anybody got annoyed by your question!

6 Likes

Exactly right, I was going to say this same thing. Could be this is what @ivan-demchenko was getting hung up on.

In addition to @tsnobip’s example consider these examples:

let x: 'a = 1
let f = x => Js.String.toUpperCase(x)
let y = f(x)

I can put that type hint let x: 'a ... there but the compiler will still know the type of x is int, because it knows 1 is an int and the int can fill in for the 'a there. And I will get an error because I’m trying to pass an int to something that takes a string.

This has type: int
  Somewhere wanted: Js.String.t (defined as string)

Above, the compiler really knows that 'a is an int. As you have found, the externals are a bit different… A thing to keep in mind is that external bindings are basically “trust me” moments between you and the compiler. Eg this will blow up at runtime even though it typechecks because you essentially lied to the compiler about the contract of the add1 function:

%%raw(`
function add1(x) {
  return x + 1;
}
`)

external add1: int => 'a = "add1"

let toUpper = s => Js.String.toUpperCase(s)

10->add1->toUpper->Js.log

The compiler has enough info to determine that 'a should be string in this program, but that is based on a bad external definition, and so you get runtime errors. In particular, that 'a in the external being unconstrained by the input type is what leads to issues.

For fun check this out (using the same add1 external from above):

let thing = {
  add1(10);
  2
}

gives the warning: this statement never returns (or has an unsound type.). That gives you a bit of an idea of what is going on here…externals are one way to break the type safety and introduce the unsound behavior.


Anyway, maybe you will find these examples interesting.

4 Likes

I cannot thank you enough for all of your answers, links and gifts of knowledge. I appreciate your time and patience - it takes effort to explain all these details. You are an amazing community!

I’ll take my time to go through all of the materials and comments and connect the dots in my brain.

7 Likes

The technical name for this is weak polymorphism, and it has a detailed explanation (if you can handle OCaml syntax) in the OCaml manual:
https://v2.ocaml.org/manual/polymorphism.html

This also leads to the “value restriction” (explained on the same page) which is sometimes seen as a drawback compared to pure functional programming languages like Haskell.

I tend to think of it as a gradual type. When the 'a type is returned (in your case it’s by an external method), it means the compiler doesn’t have enough information yet to determine what the type is. The first time the value is used it makes a note and then enforces it like any other type.

What makes this safe is that it’s all done at compile time. If you tried to leave code such that the value remains a weak type you’d likely get a type error.

3 Likes

I don’t think in this case that the 'a is a weakly polymorphic type variable.

Weak type variables are placeholders for concrete types. They’re not actually polymorphic at all, but monomorphic, and can have only one type…it’s just that that one type is delayed in its instantiation.

(Here is the quote from the linked manual section:)

A weak type variable is a placeholder for a single type that is currently unknown. Once the specific type t behind the placeholder type '_weak1 is known, all occurrences of '_weak1 will be replaced by t.

If the type variable was a weak type I couldn’t do this:

external yo: unit => 'a = "yo"

let a: int = yo()
let b: string = yo()

But I can, because it is a polymorphic type variable. (Though that external being bound probably isn’t really polymorphic so probably some runtime error will happen, but the compiler won’t stop it.)

And I can do the same thing in the original example (playground):

module MyComponent = {
  @react.component
  let make = () => {
    <>
      <input
        onChange={evt => {
          let _x: int = ReactEvent.Form.target(evt)["value"]
          let _x: string = ReactEvent.Form.target(evt)["value"]
        }}
      />
    </>
  }
}

That’s in contrast to something like this that is weakly polymorphic, ie a placeholder for a single type.

let x = ref(None)
x := Some(1)
// x := Some("yo") // won't work

Kind of cool, but I can do this and generate a weak type variable

external yo: unit => 'a = "yo"
let x: Js.Array.t<'a> = yo()

but that’s because of the Array and potential side-effects.

1 Like

But that’s not using a type variable in a weak position. I’m not saying all 'a type variables are weak.

You’re right, the updated example shows that the type of ReactEvent.Form.target isn’t weak. However, when you do let x = ReactEvent.Form.target(evt)["value"] then the type of x is weak and filled in by the compiler later - the same way it is for the ref example (where ref<'a> is not weak, but let x = ref(None) is).

It’s a subtle difference :slight_smile:

1 Like

I think I understand what you are getting at, but I’m not sure that is correct either. (I apologize if I am just misunderstanding what you are trying to say.)

Weak type variables cannot escape the context of the compilation unit (at least in OCaml, correct me if I’m wrong in Rescript), and so I couldn’t include something with an non-generalizable type variable in something that can be accessed outside of that compilation unit.

But I can do this:

Yo.res

%%raw(`
function yo() {
  return 1
}
`)

external yo: unit => 'a = "yo"

let x = yo()

Yo.resi

external yo: unit => 'a = "yo"

let x: 'a

YoConsumer.res

let x = Yo.x
Js.log(x + 1)
Js.log(x ++ ", hello!")

let y = Yo.x
Js.log(y ++ " weird")
Js.log(y + 10)

And that all compiles fine. If Yo.x was given a weak type by the compiler that would not work. The type isn’t weakly polymorphic, it’s actually polymorphic.

(I’m using an external instead of the ReactEvent.Form.target(evt)["value"] but it works the same way.)

1 Like

That’s not exactly what I was talking about, but I modified the original example to do the same thing and it also compiles. Fair point.

2 Likes