loading...

Get compiler help, even with “stringly” typed parameters in ReasonML

splodingsocks profile image Murphy Randle ・2 min read

Sometimes types aren’t enough to help the compiler help you. Sometimes you need to change a function parameter from one string to another string (for example, you want a user’s ID all of a sudden, instead of their name). The semantics are critically different, but the type system has no way to help you pass in the right thing, and you’re left to wanter the codebase on your own, searching out every invocation of the function and hoping you don’t get failed requests because you passed “Sally Mae” to the user service instead of “aw3432refq23f23qfqa23f” (what, your user IDs don’t look like that?)

For example, say the old function looked like this:

let getUser = (userName: string): Js.Promise.t(Js.Json.t) => {
    UserService.getUser(userName)   
};

But now you need it to look like this:

let getUser = (userId: string): Js.Promise.t(Js.Json.t) => {
    UserService.getUser(userId) 
};

Uh oh! The only thing that changed was the name of the “userName” parameter to be “userId”. You save the file, the compiler reports 0 errors, you ship, and the app breaks.

In this case, we can use ReasonML’s labelled arguments to tell the compiler that we need to change all of the invocations of this function.

let getUser = (~userId: string): Js.Promise.t(Js.Json.t) => {
    UserService.getUser(userId) 
};

See that little ~ you threw in there? Adding a tilde (what a strange word) at the beginning of the function name requires that any function callers specify the correct name of that parameter on invocation, and that’s enforced by the compiler. So now at the call site, which looks like this:

getUser(user.name);

You get a compiler error! Every call site gives you a compiler error. You get a wry smile on your face as the terminal leads you through 263 errors, each one fixed simply by changing the code to:

getUser(~userId=user.id);

Except you probably don’t have the user in scope at one of the call sights, and you have to spend the next three months refactoring your application code to get the userId there, instead of the user name. It's okay, it happens.

But, finally, the compiler reports 0 errors. You ship your app. You get no complaints, and you head out for a dip at the beach with your besties. 🏝

Have fun!

Discussion

markdown guide
 

You could also go:

type userId =
  | UserId(string);
let getUser = (userId: userId) /* ... */

Sure, calling getUser(UserId(user.id)) is as verbose as getUser(~userId: user.id)—provided you want to leave those naked strings in all the other places. But maybe it makes more sense to have app-wide userId type anyway and not let strings wander any further than JSON encoding/decoding and suchlike.

You could probably also use phantom types, but I’m not sure if those are easy to use across modules, or whether putting all the user-related business logic in the User module is actually better than just an app-wide type. I’m still new to functional languages.

 

Definitely other good approaches. Thanks, Sergey!