Still going strong - but how do I become a guru?

Hey guys, I’ve been at it for a while. I hope everyone is still well, and I hope you’re still enjoying ReScript. I had a pause from it, and I got back into it a while back.

Alright, so first things first - it was actually difficult to get back into it, but once I did, I was in the zone! Here’s an amazing anecdote. I had an old ReScript code base of mine, and I refactored it to be up-to-date with my more recent level of expertise (hah!). I started refactoring - I couldn’t get anything to compile for a couple of hours. As you’re probably familiar with, changing stuff around in ReScript gives a kind of domino effect (especially if you start swapping out 3rd party libs throughout - hello rescript-future, bye rescript-promise [sorry @ryyppy]). But once it compiled, it ran without any issue whatsoever. Amazing.

Well ok, so, now I want to become a guru. I want others to look at my ReScript code and just feel “hey, that guy must be from the fourth dimension.” I want it to be performant and concise, but also beautiful. I love code conventions and idiomatic code - but I haven’t found any good sources on it. In particular, I feel it’s difficult to manage chained promises (I tried async/await in ReScript - didn’t like it) and futures. They quickly turn into a mess.

So what are your thoughts on this, and what does your code look like, and what are some nice patterns or just ideas for writing good ReScript code?

4 Likes

FWIW, I think async/await for chaining promises works best if you embrace exceptions by way of early returns. If you use result, I guess async/await doesn’t give you too much because you still have to write monadic code. Then rescript-future or reason-promise are your friends.

As for your question, I think one of the reasons there’s not a lot of modern articles and tutorials on advanced stuff is because there is still no consensus. However much the maintainers want ReScript to be an approachable and practical statically typed Alt-Js language, a lot of its users and especially contributors are still people with background in OCaml and other functional/ML-like languages, so their opinion on what should be idiomatic differ from that of a core team’s.

3 Likes

(Sorry this is kind of long and rambling, but hopefully it adds to the discussion.)

This is a really interesting point. Basically everything I know about ReScript is OCaml knowledge transferred, plus some stuff about how certain ways of writing code generates better compiled JS than others. But the thing is, ReScript seems to be pushing you into a bit of a different style than you may write OCaml.

One example is having no let operators (or infix operators)…when you are used to them for writing monadic/applicative code, you may want to write it a certain way, or be more likely to use that style in the first place. But ReScript lacking those, makes it (imo) more tedious to work with that style of code. So you may be more likely to avoid monadic/applicative code. Now, I’m not saying that one way is better/worse than the other, just that it is different. And that ReScript seems to be pushing you to write in a slightly different way than you might with OCaml.

Another example is that certain styles generate better JS output than others. There have been a lot of interesting posts/replies on the forum about getting nice generated JS. What’s interesting is that certain styles of coding will generate better JS, and certain styles/constructs will generate worse JS. So again, it’s another way that the ReScript language itself is pushing you to code in a certain style or to favor certain constructs over others. And that style may not be exactly what you want to do if you’re an OCaml programmer. (Again, that’s okay, ReScript is it’s own thing.)

So yeah, it’s really easy to just think, well, ReScript is really close to OCaml in a lot of ways, (ie used to be bucklescript), and I’m an OCaml programmer, so I will write ReScript just like OCaml and be (mostly) fine. But that really doesn’t seem to be what ReScript itself is pushing you to do. You can do it that way, but is it really the nicest, most idoimatic way? Maybe not.

To circle back around to the original question about idiomatic/nice/cool/clear ReScript code…when you need “advanced” info, since ReScript is still so new, often the easiest option is to read material/libraries for/in OCaml, or to see how something is done in OCaml. But ReScript itself is subtly pushing you to write code in a way that may not line up with how you may want to approach something in OCaml.

Super long winded way to say, I think the OP question is a really good one that I also have and am curious to hear from more experienced ReScripters about how they approach it.

3 Likes

I’ve tried multiple times working with results but my codebase always ends up verbose with 90% of it being just unwrapping the results. In the end, I find it typically ends up as a thrown error anyways so the effort isn’t worth it

So I’ve moved towards using exceptions in my async/IO stuff, accepting the fact that async is typically unsafe, and using reanalyze to warn if I’m missing any catch clauses. Then I try to keep my promises flat, fetch what I need, and pass it into a “pure core” that follows a bit more functional style

@raises(ConfigError)
let readConfig = async path =>
  try {
    let content = await FSP.read(path)
    switch validate(content) {
      | Ok(value) => value
      | Error(reason) => raise(ConfigError("..."))
    }
  } catch {
    | Js.Exn.Error(err) => raise(ConfigError("..."))
  }

let build = (~path) =>
  try {
    let config = await readConfig(path) // read
    let artifacts = generateBuildArtifacts(config) // transform
    await Build.write(artifacts) // write
  } catch {
    | ConfigError(reason) => Js.Exn.raiseError(reason)
    | BuildError(reason) => Js.Exn.raiseError(reason)
  }

I find this split really helps me with following what’s going on, and write in a way that’s much easier testable.

I take this approach coming from a TS background with no Ocaml experience

9 Likes

Whoa, didn’t know about @raises. I should really start using Reanalyze.

It’s not required to have knowledge about OCaml to be productive with ReScript. I personally came from JS and didn’t have any problems even though I’ve never touched OCaml in my life. I find it exciting to have people with different backgrounds sharing their experiences.

5 Likes

Sometimes if I get tired, I cheat a bit to prevent this domino effect. I like having the compiler green sometimes.

I cheat by picking a layer or function(s) that I can stub with the correct types but fake values. I comment out the original body of code. In Rust, they have an a macro “unimplemented!” that lets you achieve this. I’m unsure what equivalent we have in rescript.

Then I can commit and breathe lol. Once I return, I remove the stubs and uncomment the original body to resume work.

1 Like

I use failwith for this…

let x: int => int = _x => failwith("todo")

let y = 1 + x(10)

Edit: playground

4 Likes

Ive had some good success using Result as a monad with polymorphic variant on the error side.
This lets you do “railroad” programming on the Ok side, and accumulate a variant of possible errors through your various steps that you can resolve at appropriate moments.

We end up with, like:

          | Error(#Validation()...
            | Error(#Apollo(_) as e)
            | Error(#Graphql(_) as e)
            | Error(#BadBody(_) as e)
            | Error(#BadStatus(_, _) as e)
            | Error(#FetchFailure(_) as e)
            | Error(#ParseError(_) as e) => 
4 Likes

oh my. perfffectttt. This doesn’t show up in the rescript docs. Maybe we should add it somewhere.

I don’t know if this is an off-topic answer. It may be far from how to write elegant ReScript code.

I acknowledged the lack of an entire ecosystem and chose to work better with TypeScript rather than do everything in ReScript.

The two languages have very different advantages, for example, when I need to build a system with onion architecture,

ReScript is useful for writing core business service logic close to the domain. Powerful type system and pattern matching!

However, for integrating with the infra layer and adapters, TypeScript is superior. Building a GraphQL API with pothos, DB integration with Prisma, etc.

So why not use both of them? Luckily we have a great tool such as genType. All we need is a trivial pattern to do better based on it.

A core is, by definition, a pure set of functions with no external dependencies. It’s really simple to keep conventional and idiomatic code.

Here’s some codes

This is a lesson I’ve learned from trying out projects with ReScript for a long time. Compromising, and taking only the best of the language.

For wrestling with complex bindings, let’s be honest. It is better suited to TypeScript.

4 Likes

Welcome back, glad you returned!

This will obviously be a highly personal opinion, and also a really boring answer. But what I value and view as good ReScript code is simplicity. Keeping things as simple, straight forward and easy to understand as possible. To me, “guru” code is code I can look at and more or less immediately understand. A few specific points:

ReScript has a fairly thin API surface. To me, thin API surface = very few ways to do the same thing = code can be simpler, easier to read, and easier to maintain.

Variants is one of the main features. Work on leveraging variants (and polyvariants to an extent) to model your domain and state in a way that eliminates impossible states (so the compiler can help you as much as possible). Variants give us an opportunity to get rid of overloaded booleans and strings. And, it lets us tailor the code to feel “fluent” to read.

Strong inference means less typing, but higher risk for confusing code if you don’t name things in a descriptive way. I try to work with that a lot too.

All of these things are fairly simple and rudimentary for the language though. And that’s what I like and value. There’s a limited set of (very powerful) constructs and features that you’ll learn to leverage deeply. And because there’s a quite limited amount of ways to do the same thing, you’ll feel right at home in most code bases after a shorter while.

6 Likes

Really like the approach :+1:
Since I don’t have many app ideas for now, I’ve personally chosen to cover this ecosystem gaps. It takes a lot of time, but fun.

2 Likes

I totally agree.
I spend a lot of time for building libraries for my own private fun project but I love the idea to have one code base / one language for the whole project. So my backend and frontend is completely written in rescript. I’m using a typesafe query builder (WIP) for the database stuff, a RPC lib (also WIP) for a typesafe communication between fe and be, rescript react, …

So a lot of work but no gaps between any components AND a b"lazingly fast" compiler for the whole project.

At some point, maybe i will publish the libraries and / or the project.

4 Likes

Just fyi, you can simplify this to:

let x = failwith("todo")
let y = 1 + x(10)

Any value can be stubbed. And functions are just values. The power of ReScript comes from its simplicity.

A side note, on every ReScript project I’ve worked on, I’ve noticed the effect that having interface files (.resi) forces better, simpler design, makes the codebase easier to read (types and documentation focused in the file without implementation details), and forces the programmer to think about modular design. I feel that this thought process is the key to ReScript mastery.

6 Likes

I read through your codebase. Just the right size to pick up the patterns in a short time. Thank you for having it open sourced!

I like the way you handled the interop between typescript and res. Little things like keying off _RE, tags for errors, event store, and your abstract logic module are super neat. However, I can’t understand where you’re handling the polymorphic variant errors you return from your core. I’m expecting a fastify error plugin to catch all your thrown exceptions from the service code + graphql side.

I also use fastify. Think I’ll module my fastify declarations next to their implementations too. Kind of an annoying pattern to have to modify the context object, but it is what it is. I’ve yet to find the perfect type safe middleware api.

Think I’ll start implementing this style of code too.

As an aside, since my team is building a unity game, I’m experimenting with f# for the backend (asp.net) to unify everything onto dotnet. Hope to share how that compares to rescript day to day,

3 Likes