Domain modeling in Rescript - Resources, Patterns?

The ML family of languages are great for modeling thanks to sum types, exhaustive pattern matching etc. However, one aspect that’s still hard to grasp sometimes is modules, and how there are subtle differences compared to other languages we might be used to.

For example, for people coming from mainstream languages like TS, Rust etc. we are used to modeling things with interfaces/traits (adhoc polymorphism) and function overloading. Haskell can do these with typeclasses.

However, in the ML family of languages (OCaml, Rescript etc. which do not have modular implicits yet), we prefer more explicit behavior and the above would be achieved with functors or first class modules, Would there be any resources in Rescript for it? Are there any down to earth tutorials or resources recommended for beginners to think about domain modeling?

There seems to be some articles in F# or Ocaml, as well as books like Domain Modeling Made Functional. But for example in Domain Modeling Made Functional, they do not mention the word polymorphism or do not want to delve into functors. Curious to know if there are any pragmatic articles/resources around these topics for beginners :pray:

4 Likes

Most of the F# code from Wlaschin’s book is more or less applicable to ReScript too. We just don’t have computation expressions or the like. Also we tend to prefer simplicity over DRY, because Functors (for instance) are usually not worth the hassle if you accept just a little bit of code duplication.

I just found out that our user @hoichi did a talk about this exact topic Domain Modelling and Architecture for ReScript Apps | Talk at HolyJS 2023 Spring maybe he has some english material available as well?

3 Likes

Well, no, I’ve never published anything on the topic in English, but maybe I should. My talk took a lot of inspiration from that book of course so maybe if/when I do a writeup on that topic, I should concentrate on frontend specifics (since backend architecture might be closer to what Wlaschin describes).

One notable difference between F# and ReScript is that the latter has functors. I know that many people consider them a source of unnecessary complexity, but I think if you know what you’re doing they’re a great asset for domain modelling.

5 Likes

Since before Rescript was Rescript, I have shipped a number of production DDD services in the ReasonML/Bucklescript/Rescript world.

Unfortunately, I am not aware of any really great resources. As you have mentioned, ‘Domain Modeling Made Functional’ and all of his articles, are probably the closest things I’ve found, but it intentionally keeps things super simple, which does not necessarily scale to most systems that are big/complicated enough to benefit from DDD.

Functors are extremely useful for design, and separating models/ideas, isolating complexity. If you are just writing UI-layer code (which I believe is the majority of user-land Rescript), then it isn’t as useful. I think that is why we don’t see a lot of functor use/discussion in Rescript

But in my experience, if you are following DDD, functors are nearly irreplaceable as a design tool. For example, take repositories. The design of a repository is part of the domain design. It belongs in the domain layer. But the implementation of a repository is purely technical and belongs outside of the domain. IMO, the best way of those two things practically living in one system, and being properly disconnected, is as a separate module type and module, which are connected using a functor.

The best thing I personally did was to just read a ton of OCaml code (especially mirage-os code and systems built using it).

4 Likes

@yawaramin wrote a book a few years ago called “Learn Type-Driven Development”. It was written for ReasonML but it’s closer than F# to ReScript. You’d probably still need another resource for something that goes beyond foundational-level modeling with types.

1 Like

Ah these are all very interesting points! I’ll try to study them a bit more, thank you everyone for all the context!

simplicity over DRY
if you accept just a little bit of code duplication

Sounds good! This part brings to mind how loosely coupled designs (over DRY) incurs a bit more code duplication (forwarding calls, repeating models but keeping them separated etc.) in exchange for greatly added benefits. And this seems to be well documented on internet with the usual languages having adhoc polymorphism to do dependency injection. When trying to write code in ML languages (like Rescript/OCaml), I often have trouble seeing how to do the same thing: modeling the world loosely and enforcing contracts (OOP composition + interfaces vs a more data+functional way in Rescript?)

For example, in these other languages we could do composition over inheritance and define data types then define multiple interfaces/traits (only for behaviors) and make these models honor these contracts (even multiple interfaces). In that way, it could be still be WET (instead of DRY) but we would have clear contracts that are enforced. Golang is the epitome of this where we can inherit pure data (struct embeddings) but only compose behaviors (with interfaces and composition through struct embeddings)

I’m having a hard time reproducing these situations when iterating on Rescript. Sometimes a functor or higher module seems to be the way but it indeed feels overkill. For example, is there way to make a module Warrior honor 2 “interfaces” or is there another way (functional) to think about it? This is all in the context of modeling things on the backend (having clean code decoupled layers, creating repositories, D.I etc.) or to have fun modeling entities in a small game

Thank you @fham for the link posted! Really great to see the slides but could only read the code portions :joy: Would love to see any new publications on this subject if you make others @hoichi, thanks for making this presentation!

1 Like

Yes that’s exactly the type of modeling in question! Would also love to experiment with Rescript on the backend side with DDD / Clean code layers (especially on the edge running multiple instances of the software where reliability is important without going as far as to using Rust) Very interesting point about how Rescript is used more for the frontend so these questions arise less. Actually curious to know if we have a lot of examples of Rescript being used somewhere else like in servers, etc.

And you hit the nail with DDD and how functors seem to be the answer to modeling some of these relationships! I’m still trying to get used to this pattern and wish there were more practical resources around it :smiley: Thank you for the reference of mirage-os, sounds like reading OCaml code will be the way to get more exposure

1 Like

Oh thank you so much for this book reference! It does look like the later chapters cover these topics (Reusing Code with Many Different Types, Extending Types with New Behavior, Bringing It All Together) that’s great! I’ll try to study it

And sounds good for another resource to go beyond the foundational-level modeling, curious to know if there are any good references for this (in addition to reading OCaml code as mentioned by @kome)

Here is another article about ReScript and DDD by @nkrkv

5 Likes

Here is a very very basic example of what I do. I actually use CQRS and Event Sourcing, but this matches the majority of DDD examples out there which focus on aggregates.

The domain is the truth and is not abstracted. It is just defined as modules. I encourage you to go as deep as you want in defining your domain to be solid using type-driven design. But for simplicity, I will just use things like string:

// Domain Layer - User.res

type user = {
  email: string
  nickname: string
}

let changeUserEmail = (user, ~newEmail) => {
  // return possible domain errors
  let modifiedUser = {
    email: newEmail,
    nickname: user.nickname
  }
  Ok(modifiedUser)
}

module type UserRepository = {
  type t
  let findByEmail: (t, ~email: string) => promise<option<user>>
}

Now you will define the application layer, which uses the domain layer. Since the application layer is built to protect the domain layer, I believe that means it accesses the domain directly. So it is not passed through a functor. The infrastructure layer is of course separate and passed through the functor to separate the concerns.

// Application Layer - UserService.res

module Make = (R: User.UserRepository) => {
  type t = {
    userRepository: R.t
  }

  type error = | UserNotFound
  
  let make = (~userRepository) => { userRepository: userRepository }

  let changeUserEmail = async (t, ~currentEmail, ~newEmail) => {
    switch await t.userRepository->R.findByEmail(~email=currentEmail) {
      | None => Error(UserNotFound)
      | Some(user) => 
        switch user->User.changeUserEmail(~newEmail) {
          | Error(domainError) => Error(...) // change to application error
          | Ok(_) as newUser => newUser
        }
    }
  }
}

Then you would build something like a MysqlUserRepository module which, when passed into the functor, is limited to the interface defined by the UserRepository module type.

If you repeat this pattern again, one layer up, you can pass the Application layer into the UI layer. But at some point, you will have to initialize things.

module US = UserService.Make(MysqlUserRepository)
let userRepository = MysqlUserRepository.make(mysqlConfig)
let userService = US.make(~userRepository)
userService->US.changeUserEmail(~currentEmail, ~newEmail)
5 Likes

Awesome examples @fham and @kome! Thank you so much, I spent some time to read them and these were exactly the type of examples I was looking for!

2 Likes

I did a quick write up on using Variant types for business logic: Using variant types in ReScript to represent business logic - DEV Community

It’s based on the domain driven design book/talk by Scott Wlaschin, which are a great resource for any functional ML family language.

1 Like

Check this out for DDD practices with event sourcing in a ReScript codebase

1 Like