Any example of how to use the Dom API?

@cvr I’d implore you to read all of my commit messages and comments on there (if only because I’ve spent quite a bit of time making them…)

We’re not avoiding type safety (this also isn’t one of those cases where we’ve loosened on type safety at the external boundaries because of a low ROI). The refactored app is safe, provably so through the types, JS output differences, and yes, like you said, the strong assumptions of the behaviors of that app. We leverage those (as we always should) in order to better type various values, to reflect what they actually are.

If a value is an string, typing it as option<string> doesn’t give you more type safety. In fact, it does the opposite by lying about the number of possible states of that value (how come there’s an extra state now?). If your question is "yeah but what if at writing time I accidentally passed the wrong dom id and now it’s legit None", realize that typing it as option<string> still doesn’t help you catch it at compile time. As a matter of fact, you’ve just accidentally handled it at runtime, and potentially silenced that critical mistake, ironically by making the None state a legal state of your app. Obviously, the solution is not to silence it with a None branch, but to fix that at writing time.

It seems like a lot of code written was just plain javascript, making me feel like the value of rescript is nonexistent in those commits.

See my previous post and my previous paragraph. We should never start from “where can I make ReScript’s particular features shine” and be disappointed when they don’t fit into a use-case. That’s backward. Taking a step back, you might realize that, contrary to perhaps your own desire, you’ve accidentally justified that the code looking like JavaScript is a bad thing; it’s a great, unique value proposition! Not to mention I refactored the code into that while preserving type safety, fast compilation and other benefits. You see how starting from the shiny tech can mislead one’s own goals?

But even beside that point, it’s plain untrue that the value of ReScript is nonexistent in those commits. I refactored the code; ReScript absolutely helped during refactoring in all the typical ways we know it’d have helped. I’d invite you to try to refactor that yourself starting from the first commit and experience the value yourself.

(By the way, the original code actually had unsafe conversions, one of which I’ve removed in e5f725746aab03f910110bb8d4efdd2bcd6d4ac0 precisely thanks to the above explanations).

3 Likes

@chenglou I enjoyed reading all of the commit messages you made and the refactoring you’ve done. I’m extremely interested in rescript and using it language of choice. I recently refactored my app to use reasonml (only due to better editor support). I’m still quite new, and the only typed language experience I have extensively is typescript. As I’m soon to start a side project and want to use rescript, I find what you did extremely useful.

I should rather say for the purpose of discussion, what would you say are the advantages/disadvantages of using rescript in this particular case over typescript?

1 Like

Glad you like them! Generally speaking our value props are documented at https://rescript-lang.org/docs/manual/latest/introduction already.

In this particular case, it’s precisely what I’ve pointed out in my previous message. Consider that we’ve successfully reduced option<string> to string (that’s just an example. The actual commits in the codebase reduced spiritually similar things). In ReScript, you know when it’s a string, it’s never option<string>. Thanks to the strong assumptions of the app as you’ve pointed out, and a type system that doesn’t lie, you can sleep well knowing there’s actually no None case that you ever need to handle.

Compared this to TypeScript, where you equally try to leverage the strong assumptions of your app, and type something as string instead of undefined | string. TS’ type system being unsound means looking at a string doesn’t actually guarantee that you’ll never get undefined. So your conclusion of "there’s truly no undefined" will be shaky for various reasons.

Altogether, consider the cases we’ve discussed, and their impact on reasoning during debugging, when you e.g. pass a wrong DOM node ID during querying and produce a None:

  1. You use ReScript and type the DOM value as option<string>. Oops, that illegal state got accidentally handled by some None branch(es) somewhere. You now get a runtime message regarding some null value. But you don’t know whether this is a proper handling of a truly null value (e.g. something came from the network payload), or because you’ve made a typo in the node ID, or whatever else. You end up debugging though all the callsites that wrongly pretended that they might need to handle None. Monadic abstractions make this even worse because you’re obliged to lift all the callsites.
  2. You use ReScript and type the DOM value as string (after verifying all the invariant we’ve said above). You still get a runtime null, but because you know though the assumptions and the type that it should have never been null, you can immediately rule out mishandled branches and other possibilities, and straight up conclude that your DOM querying was incorrect. You quickly fix this at writing time and resume maintaining the assumptions.
  3. You use TypeScript and type the DOM value as string. If you get a null value, you need to debug it the same way as in case 1.
  4. You use TypeScript and type the DOM value as undefined | string. That doesn’t help much.

(Hopefully you can see how starting from wanting to use variants here would often mislead you into case 1 while proudly thinking that you’ve leveraged some shiny language feature and made your development experience better, blissfully unaware that you’ve made your coding life harder and wondering why others aren’t as receptive of your language proposition. This is a big downside of any shiny tech that we try hard to avoid.)

Hope that makes sense?

2 Likes

Ah, I see. That makes sense, thank you!

1 Like

Hi,
Cheng’s opinion is his personal opinion, I would say his suggestion is valid most of the time, but it does not mean we will enforce a single style as a community.
Your previous work is very much appreciated, people can learn something from it even if it’s not 100% correct sometimes.
As a community, we are inclusive since people may have different tech background.

6 Likes

Although I like the simplicity of the result (I was never a fan of bs-webapi’s ergonomics myself), I feel like this refactor has some shortcomings if it is to demonstrate how to approach problems such as interaction with DOM APIs. Hoping that it is useful feedback for the team, I’m going to take this refactor at face value and give my two cents.

Take this commit for example: https://github.com/chenglou/domgraphs/commit/0727144626b7a6013d297b6aab74d056cb07f889#diff-6b86dad133536de8149349ea05fd8585R51

I think this only works when you’re refactoring working software to make the code simpler. It benefits from:

  • Having some code that was correct and performed the task well in the first place
  • Being able to look at and compare the generated JS output of some existing code
  • Having a good command of DOM API’s already before attempting to write this code from scratch

It is indeed easier to read and follow in places (due to bs-webapi’s API design and difficulty of making generic DOM bindings), but I’m not sure if it is easier to write, let alone easier to write correctly. Concerns such as false sense of safety or inaccuracies regarding nullability are important things to address of course. But knowing how often I get my hand-written bindings wrong, how hard they are to maintain when the underlying JS library keeps changing, and how many head-scratching bugs they caused me in the past, I don’t want to be in the mindset of “always interacting with JS” like when writing code such as context["moveTo"](~x=0.0, ~y=centerY). Thankfully DOM API is more stable than rest of the JS ecosystem and very well documented, but I would also be afraid to have this sort of arbitrary JS access all around a big codebase only to have the underlying library change its API making it a really error-prone upgrade process (instead of updating the bindings and letting the type system help with the rest of the work).

When building something such as domgraphs I heavily rely on tooling. With JavaScript and TypeScript you get a lot of help from editors such as VS Code. It allows you to discover APIs and get your code right (although it is sometimes unsound). libraries such as bs-webapi or Fable.Browser also provide a similar boost when interacting APIs as complex as DOM. Of course it is not a replacement for having a good working knowledge of the underlying API, but I would always want more assistance when writing code than having to have MDN or some JS libraries README always open on the side.

Another way of looking at it; Old browsers or different execution environments not having some features doesn’t invalidate the entirety of https://github.com/microsoft/TypeScript/blob/master/lib/lib.dom.d.ts, and I’m glad there’s a peer reviewed collection of types for DOM used by many people and written by people who have much more experience than I do.

For example, with let width = Belt.Float.fromInt(element["width"]) I definitely see myself omitting Belt.Float.fromInt if I don’t know the return type by heart. Or whether it can be null or not. In JS there’s already a culture of overly defensive code such as typeof element.width = "number" ? ... : .... People don’t start out writing code this way, but after one too many easily avoidable errors on Sentry, they adopt this pattern to interact with any and every function someone else wrote.

Re-creating all of this ecosystem of bindings with a small community and gargantuan API’s such as DOM that don’t always align with ReScript’s type system is an uphill battle indeed. Maybe ReScript tooling (not necessarily the compiler, but definitely the editor) can help out with this if it can seamlessly utilize TypeScript types to display hints when interacting with JS without bindings. Some editor features that would help with the following line
context["fillRect"](~x=0.0, ~y=0.0, ~w=width, ~h=height) :

  • I would like to have fillRect suggested as I type
    Personally, it is always a pain if I need to stop and google this sort of stuff. I might not know what this method is called, or how to spell it, or whether it exists at all. Autocomplete is a massive productivity boost.

  • I would like to see these labelled arguments suggested as I pass arguments.
    I really have no idea what order I need to pass them. Using labelled arguments help as documentation. But I first need to figure out what each of these positional arguments in one specific overload of the original JS function stand for and the accepted types.

    I also don’t know if I’m passing an unused argument to this function, or whether I need to (or can) pass more arguments.

    Slightly off topic; using labelled arguments in this case makes me think that I can pass them in a different order or I can insert another argument in the future at an arbitrary position, which is not the case.

  • I would like to be able to jump to definition and read signatures, docs. The destination could be lib.dom.d.ts#L3397

Of course this doesn’t help with poorly documented JS libraries much. There’s too many popular libraries with a single README.md acting as documentation containing things like “drawUnicorn: takes a canvas and draws a unicorn according to the options passed as the second argument” and having no other information. At least with bindings someone figures this stuff out once which helps everyone else. Unfortunately I also often don’t have the luxury of being able to say “I won’t use this poorly written library” or “All of the code we interact with in our project is bespoke apart from platform APIs”. I can’t count the number of times I was able to find someones hand-written Flow or TypeScript documentation for a JS library on Github without which I was unable to figure out how to use the library correctly in JS. I don’t think “with ReScript you don’t need bindings” tagline really clicks with me.

10 Likes

I’m quite new to ReScript, but more experienced with TS comparably. I can’t clearly understand what you’ve mentioned here. If I type something as string in TS, what makes TS can’t actually guarantee that it will never get undefined? How does ReScript achieve it over TS?

2 Likes

Hey @osener

but I’m not sure if it is easier to write, let alone easier to write correctly

I’d encourage you to try both ways and experience for yourself.

Regarding the other comments: The last step of the process would be to solidify those canvas helpers into a couple of externals, which means you won’t get them wrong. I’ve omitted that in the commits.

@moondaddi hello! Then you might have heard of various unsoundness of typescript. Tldr TypeScript’s type system is best-effort only for various reasons. If you google that you’ll find some links and examples.

ReScript achieves it because it was built from the ground up to be sound first and foremost (using OCaml’s type checker under the hood).

Well I’ve been using Reason+BuckleScript in production with projects and teams of different sizes for three years and have done this every which way imaginable. This was an honest try at giving some feedback, but I won’t comment on this any further.

1 Like

@chenglou I think I got your point and I read the doc again says about the differences between Typescript which is https://rescript-lang.org/docs/manual/latest/introduction I agree with your point and it could be the best choice for code readability and performance. But I have a small example here.

let (area, setArea) = React.useState(_ => 0.0);

let handleChangeArea = e => {
  let value = e->ReactEvent.Form.currentTarget##value;  // untyped value here
  setArea(_ => value);  // value should be converted to float from string
}

<input type_="number" onChange=handleChangeArea />

In this case, I can’t get any warning or error during write-time or compile-time. Only in runtime, I would find the error that I should convert the value to float inside setArea. I’ve done a couple of projects with ReasonML, I’ve been always facing the moment to make a decision somewhere around the boundary of interop to the outside of an untyped system such as DOM.

I think we all know that the value is always a string in DOM. But this error could happen in the runtime. So, writing codes based on the best assumption, and fixing in debugging time is not what I expect from ReScript. I would like to say that what I expect most from ReasonML and ReScript is reducing error in runtime.

1 Like

@osener I don’t believe I’ve tried to offend with what I said; we don’t know each other so I was asking you to consider it if you haven’t, which you subsequently said you did.

We accord more credibility when a user speaks from a position of having tried various different scenarios over long time. And I’ll accord that to you. Also, I think I did address all your other valid comments with my last paragraph.

@moondaddi I’m well aware of this particular case. You could try typing this with webapi and tell me what happens. The original ReasonReact used the approach you were thinking about.

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. See my 4 cases in my reply to cvr. You’d end up with case 1.

In the same vein of idea:

I think we all know that the value is always a string in DOM.

It’s not; see the hash map comment. Hope you see what I mean now. That’s precisely why letting userland make their own external value accessor, which they can use on their specific, well-constrained target type and which returns string, lets you prevent this bug.

2 Likes

All the messiness of DOM APIs notwithstanding, I think all the osener’s points are valid, and to the extend quality bindings to DOM are at all possible, it would be great for us devs if this problem was solved in one place for everybody. I understand that it’s not likely to be solved in the near future, but without solving it at all, I think ReScript is going to be a hard sell to a lot of organizations.

4 Likes

it would be great for us devs if this problem was solved in one place for everybody

There are several topics here concerning what I first wrote.

  • One is type safety, where there’s the misconception of loss of type safety, but in reality there isn’t. Lack of final commit for turning the canvas method accesses into external helpers notwithstanding.

  • One is folks’ general tendency of trying to stuff a shiny feature into a product instead of working from the product back to the tech, which I’d actually like more discussions on (not here though), which is valid disregarding the features discussed in this thread and very under-discussed.

  • Another one is is my comment of removing most of my own refactor in my link due to the UI needing wholesale revamp, obsoleting most of my own effort and our technical discussions here. I kinda wished someone asked about that. This should be our priority number 1. If anyone starts a new thread on this discussing the real valuable things on this topic, I’d be very happy =). But before obsoleting most of discussion here, I wanted to make sure that the basics are clear.

  • Last one is per-app specialization of bindings vs a single global binding, that you’re talking about. I felt like I’ve explained enough that for the case of DOM, this isn’t a good idea. We’ve already established that a complete and sound DOM bindings will end up being giant hash maps of option<'anything> . That’s worse for developer ergonomics *. The same time it takes you to learn those binding apis is better spent if you just bind to your subset (because you definitely aren’t using the full DOM), encapsulate them in a few places (like in my last commit) and use those. Most of what osener said still applies. Well, there are some nice little object/records you can put here and there, but surface area isn’t the same as usage frequency. See moondaddi’s use-case, which is most of what devs use.

Btw the last one really isn’t much of a controversial opinion; I’m not sure whether it’s because folks feel like the grass is greener on the other side, or some other reasons. If that’s the case, here are some random google results on TypeScript having the same problem, needing the same type of solution: https://github.com/microsoft/TypeScript/issues/299 https://brightinventions.pl/blog/5-ways-to-benefit-from-typescript-in-react. Even with TS’ rather intense type system, this thing is relegated to userland patches. At least we’re advocating for clean, specialized bindings instead of casts!

This thread might be getting too bloated for the question. But I’d like to add hopefully one last:

  • Solving things in one place for everybody is less of a good idea relative to what you might think.

Of course, as lang/ecosystem developers, we do try to solve things in one place, to a reasonable extent. But far too often you’re much better at properly solving your one or two use-cases, than we are at solving hundreds of use-cases at once.

Also, for hoichi or anyone else: if you’d like, you can pass me some of your public ReScript DOM code, and I can attempt to tweak them using what I said above. I’ll be happy to do it.

* Some nuances:

  • For canvas in particular, it’s actually reasonable to have a single bindings location in stdlib. So we could do that.
  • General DOM api bindings in stdlib is still possible, for a select subset of DOM apis that are clearly bindable without being obtuse. But again, the bulk of your use cases are like querySelector, target, value and others, which a general, centralized binding doesn’t help with. As seen in the TS links above, that’ll end up with us binding to e.g. target as an abstract type.

This has been a nice thread btw. I’m glad we’re surfacing these perspectives and engendering better thoughts.

4 Likes

In our experience this applies really well to the general “Bindings dilemma”: When Reason / BuckleScript was pretty new (like 3-4 years ago), ppl started writing bindings for JS libraries and quickly realized that 1) it’s really hard to type the whole library, 2) that the bindings highly depended on the use-cases of the organization using it, and 3) it’s hard to keep the api similar to the JS counterpart.

So they had to do a decision: only type the parts they use in a highly specific way, or try to spend a huge amount of time trying to type the whole surface. Relying on a crowd-sourced solution is hard, because you need to find the right ppl that have the same set of use-cases, and also are in the same mindset of api design.

What I learned from talking to a lot of production users is that companies tend to build their bindings in private, because they don’t think they are generally applicable for a broader audience; in the best case they contemplated on cleaning up their bindings and then release them on github… I think until now there have only be a handful of bindings that actually made it that far. Oftentimes the upstreaming was followed by abandoning the bindings, which is also a huge burden for us to keep up with.

The second attempt we saw (actually multiple times already and ppl burned out hard on that one): Try to automatically convert the TypeScript d.ts files to ReScript. This general idea kinda worked for a subset of d.ts files, but quickly broke down on any other non trivial definition of popular JS libraries. One might assume that the type systems are kinda similar enough to do this kind of conversion, but TypeScript is an unsound type system, so you will struggle with every type hole you stumble upon.

Anyways, I agree, maybe there must be some middle ground where we can provide some support for common DOM scenarios. I think there can be this hybrid solution where ppl grab a specific set of properly curated, very basic bindings just to get going and adapt them to their needs.

It’s definitely not easy at all, but we also don’t want to gate-keep users out that just want to build a product, but get stuck every other keystroke because they think they absolutely need to properly write generalized bindings for every little detail they use in their app.

2 Likes

I think at the end of the day, it all depends on how skillful you expect the ReScript teams to be. I know Paul Graham said that in the long run, standard libraries (and by extension, bindings) don’t matter, but I’m not sure an average frontend dev coming from JS/TS is as talented a hacker as Paul Graham.

I’ve worked with quite some people that saw TypeScript in general as a hindrance. In fact, even, say, React experts like Kent. C. Dodds only add types after they’ve figured out the logic. And TypeScript is closer to JS semantics and comes with excellent IDE support: autocompletion, parameter hints, and other productivity boons. Personally, of course, I likeReScript way better; my point is that a lot of working developers don’t have a firm grasp on types in the first place, and expecting the teams to roll their own bindings seriously narrows the potential reach of ReScript. (Also, even being able to write those bindings, I think most devs would rather spend their time modeling data and programming business logic, not being lost in technicalities.)

Having said all that. Firstly, after all of the explanations, I now see the scale of problems with generic DOM API bindings better. And secondly, I very much respect the ReScript team’s decision not to offer the community the half-baked solution, riddled with false security, bloat, and so on. Thank you, people!

13 Likes

@hoichi yeah, I think that’s a good wrap up to this discussion unless folks have other questions about the main topics. One big clarification though, so that folks reading it don’t get the entirely opposite conclusion from your post:

We are not aiming for the language philosophy PG advocates. I hope that this is clear throughout this post and my refactors.

  • Contrary to PG’s opinion, standard libraries absolutely matter for day-day programming. We’re not a research language.
  • Bindings matter to us. Heck, in terms of pure quantity of bindings, you saw that I was advocating for more bindings instead of less (less, in this case, would have been to e.g. putting them in a single place in stdlib).
  • And so what we’re demonstrating here is not how experts should do it. We’re doing the opposite: throughout this thread we’ve explained how to simplify the codebase, how to remove the extra learning overhead of several language concepts and a big library, and doing so without losing type safety (if this last one sounds debatable, see my explanation in my previous post).

So we’re actively trying to simplify things, precisely for the masses of programmers. It’s the process of simplification that requires expertise, but once that’s demonstrated, any dev can imitate it. The result of the refactors above shows that; the end result is newcomers-friendly, as opposed to attempted usages of libraries and language features even experts have trouble navigating.

expecting the teams to roll their own bindings seriously narrows the potential reach of ReScript

For now, we can never truly remove the need to understand bindings, because even installing a pre-written binding requires basic understanding of what the library did if you want to use it in a nontrivial way (though we do try to diminish such need by making ReScript data structures map to JS more cleanly). Consider that if we assume bs-webapi is the way to go, a newcomer will definitely have to understand what’s going on in there to use it.

If bindings aren’t avoidable, it’s better to fully expose and document them instead of trying to hide them and let newcomers be confused longer.

6 Likes

I can’t disagree with your approach. Thank you!

FWIW, it matches well with React’s approach of being as explicit as possible and relying on the basic understanding of language features. I think it’ll be a big plus for everyone using ReasonReact.

2 Likes

I was busy when you pinged me, sorry for not responding earlier, and I mostly forgot about this until today. I’m now glad I did because I would have had a very angry response. I’m glad to see some of the other responses were along the lines of what I would have said.

I am disappointed. I thought you had bigger goals for ReScript than toy projects and small bespoke applications. In big multi-layered projects such as the one I work on your resulting code is completely unacceptable. I’ve read your commit messages but that only solidifies my impression of the style you seem to be targeting with this approach.

I am not at all interested in the simplest possible application code that produces identical JS to more complex compiler-enforced bindings. This to me sounds like throwing away significant benefits the type system can provide for some misguided sense of convenience. I need a library that developers who are familiar with developing in TypeScript using lib.dom.d.ts will be comfortable with; those are the people I think should be the most interested in ReScript. Certainly that’s the sort of developer who I will be guiding as my company expands our use of reasonml/rescript to other teams.

An open Js.t document object that lacks a list of available methods and won’t even trigger a compiler error from a typo is not something I will ever ask developers to interact with. Pretending things aren’t optional when the DOM specification says they could be is also not interesting to me; the codebases I work on are so large and so deeply tied to the DOM that such assumptions can (and have) bitten us through weird edge cases customers find when the null/undefined case actually comes up.

Finally, faster build times don’t really interest me as much as smaller bundle sizes (this is becoming a wider issue with ReScript). Our TypeScript code takes 60-90 seconds to clean compile so even a slow ReScript build is a huge win.

I might look to use your techniques in my library implementation where a direct external is not appropriate, but the application code that uses the library will never look like the style you seem to be aiming for.

The more time that goes by the more I’m convinced I will end up rewriting/forking bs-webapi to build a pipe-first version of it that is more approachable, leveraging records-as-objects and poly-variants-as-strings.

9 Likes

I don’t want to get knee deep into the argument, but I’d (also) consider it important to recognize that not all dependencies/bindings are created equal.

Some 3rd party JS libs get a version update every few months and might contain an API that is hard or even impossible to type universally. Even the code quality and internal type safety might be questionable at best. In those cases it’s probably good to be practical and implement simple local bindings, finish up early and go spend time with family or friends.

However, DOM API is part of web platform. It’s something that pretty much all of us must use. It also doesn’t change all the time, so maintainability from backwards compatibility point of view is not such a huge issue. It might be complicated to bind some specific cases and the API is not very “functional” by nature, but we as a community should still aim for the most accurate and safe representation because the work will benefit pretty much everyone.

Fwiw, after the initial learning struggles, I quite like the current Bs-Webapi. There are things to fix/improve but nothing impossible. I’d also gladly use a pipe-first version.

3 Likes