It’s an off-topic, but we discurrage the usage of the type=“number” in our projects: Why the number input is the worst input - Stack Overflow
Also, we have a UI library, and never work with react event directly, so it’s not a problem when you have a higher lever abstraction.
@DZakh The UI library is a good example of circumventing (or covering up) this kind of loophole. Here we discussed the case of handling a React event. But what about other scenarios? A strict type system should prevent such loopholes in all kinds of programs.
I understand that the {..}
syntax in Rescript is similar to { [key: string]: any }
in TypeScript, which can be read as whatever-object
or someone will handle that later
. It doesn’t help in the larger code bases, maintained by a larger group of people.
I apologise if I have offended anybody. I was just trying to understand how ReScript works so that I can promote it. I believe in the project and would like to advocate for it.
I’m not sure why my post was marked as spam.
Yes, you are 100% right. And that is why we don’t use objects in our codebase, only records. Although,{..}
is still useful for converting js code to ReScript.
No idea why it was flagged as spam, it seems to have been flagged automatically by the system. I’ve de-marked it. Nobody is offended! Quite the contrary, this is a good and valuable discussion.
Makes sense! Sounds like a call for a pull request to rescript-react
.
I added a PR for a review on rescript-react
project (I cannot paste a link to GitHub here, sorry). This is my first-ever contribution to a Rescript project, so I’m open to any constructive feedback.
@zth I figured out why this thread (and a second one I posted) got marked as spam: I tried to paste a link to github (rescript-react repo).
This is an interesting point you are bringing up…in fact, this same question is asked here: Any example of how to use the Dom API? - #25 by moondaddi. That link is also a very interesting thread that got too spicy and got locked, but it is an interesting read nonetheless. (Including the git commit messages that chenglou wrote.)
I think the response here is worth considering:
In case you don’t feel like trying:
currentTarget
is exactly one of those that are almost infinitely polymorphic. The keys are not fixed, the types of values are not either. The best you can type it, if you went with a general approach, is a hash map of string to anything. Which still doesn’t get you any compile-time benefit.
I looked at your pull request and returning Js.Json.t
still doesn’t give you compile-time type safety…you still end up with runtime checks. And if that is the case, wouldn’t it be better to embrace the fact that target
is highly polymorphic, too much so for a general binding, and write a binding specific to your app’s usecase, eg
let intValue = (evt, default) => {
let v = ReactEvent.Form.target(evt)["value"]
switch Belt.Int.fromString(v) {
| Some(x) => x
| None => default
}
}
@Ryan, thank you for your response! I agree with your approach. My only concern was that the compiler didn’t ask me to do anything with v
(from your example). It was silent about a potential runtime issue. In fact, the type mismatch wasn’t subtle. It’s right there:
- here
x
has type'a
- here
x
has typeint
The only reason why I annoy everyone with this question (I apologise for it) is the people who are not used to strongly typed languages. I am personally tired of JavaScript, TypeScript and the surrounding ecosystem - pure overhead. But I love how clean and lightweight Rescript is and I want to bring that into organisations. A strong selling point is next-level type safety (that can be translated to business value), not that the language is cool (management won’t buy it).
However, when I looked at how 'a
turned into int
(screenshots above), I was surprised it was possible. Maybe, I don’t have a deep enough understanding of the type system. But I think HM inference should show a mismatch. Alternatively, there’s a bug somewhere.
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
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!
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 external
s 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.
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.
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.
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.
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
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.)
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.