A Proposal: Elixir-Style Modules in JavaScript

Posted on by in Development, Elixir, JavaScript

Moving your code towards a more functional style can have a lot of benefits – it can be easier to reason about, easier to test, more declarative, and more. One thing that sometimes comes out worse in the move to FP, though, is organization. By comparison, Object Oriented Programming classes are a pretty useful unit of organization – methods have to be in the same class as the data they work on, so your code is pushed towards being organized in pretty logical ways.

In a modern JavaScript project, however, things are often a little less clear-cut. You’re generally building your application around framework constructs like components, services, and controllers, and this framework code is often a stateful class with a lot of dependencies. Being a good functional programmer, you pull your business logic out into small pure functions, composing them together in your component to transform some state. Now you can test them in isolation, and all is well with the world.

But where do you put them?

Common patterns

The first answer is often “at the bottom of the file.” For example, say you’ve got your main component class called UserComponent.js. You can imagine having a couple pure helper functions like fullName(user) at the bottom of the file, and you export them to test them in UserComponent.spec.js.

Then as time goes on, you add a few more functions. Now the component is a few months old, the file is 300 lines long and it’s more pure functions than it is component. It’s clearly time to split things up. So hey, if you’ve got a UserComponent, why not toss those functions into a UserComponentHelpers.js? Now your component file looks a lot cleaner, just importing the functions it needs from the helper.

So far so good – though that UserComponentHelpers.js file is kind of a grab-bag of functions, where you’ve got fullName(user) sitting next to formatDate(date).

And then you get a new story to show users’ full names in the navbar. Okay, so now you’re going to need that fullName function in two places. Maybe toss it in a generic utils file? That’s not great.

And then, a few months later, you’re looking at the FriendsComponent, and find out someone else had already implemented fullName in there. Oops. So now the next time you need a user-related function, you check to see if there’s one already implemented. But to do that, you have to check at least UserComponent, UserComponentHelpers, and FriendsComponent, and also UserApiService, which is doing some User conversion.

So at this point, you may find yourself yearning for the days of classes, where a User would handle figuring out its own fullName. Happily, we can get the best of both worlds by borrowing from functional languages like Elixir.

Modules in Elixir

Elixir has a concept called structs, which are dictionary-like data structures with pre-defined attributes. They’re not unique to the language, but Elixir sets them up in a particularly useful way. Files generally have a single module, which holds some functions, and can define a single struct. So a User module might look like this:

Even if you’re never seen any Elixir before, that should be pretty easy to follow. A User struct is defined as having a first name, last name, and email. There’s also a related full_name function that takes a User and operates on it. The module is organized like a class – we can define the data that makes up a User, and logic that operates on Users, all in one place. But, we get all that without trouble of mutable state.

Modules in JavaScript

There’s no reason we can’t use the same pattern in JavaScript-land. Instead of organizing your pure functions around the components they’re used in, you can organize them around the data types (or domain objects in Domain Driven Design parlance) that they work on.

So, you can gather up all the user-related pure functions, from any component, and put them together in a User.js file. That’s helpful, but both a class and an Elixir module define their data structure, as well as their logic.

In JavaScript, there’s no built-in way to do that, but the simplest solution is to just add a comment. JSDoc, a popular specification for writing machine-readable documentation comments, lets you define types with the @typedef tag:

With that we’ve replicated all the information in an Elixir module in JavaScript, which will make it easier for future developers to keep track of what a User looks like in your system. But the problem with comments is they get out of date. That’s where something like TypeScript comes in. With TypeScript, you can define an interface, and the compiler will make sure it stays up-to-date:

This also works great with propTypes in react. PropTypes are just objects that can be exported, so you can define your User propType as a PropType.shape in your User module.

Then you can use the User’s type and functions in your components, reducers, and selectors.

You could do something very similar with Facebook’s Flow, or any other library that lets you define the shape of your data.

However you define your data, the key part is to put a definition of the data next to the logic on the data in the same place. That way it’s clear where your functions should go, and what they’re operating on. Also, since all your user-specific logic is in once place, you’ll probably be able to find some shared logic to pull out that might not have been obvious if it was scattered all over your codebase.

Placing Parameters

It’s good practice to always put the module’s data type in a consistent position in your functions – either always the first parameter, or always the last if you’re doing a lot of currying. It’s both helpful just to have one less decision to make, and it helps you figure out where things go – if it feels weird to put user in the primary position, then the function probably shouldn’t go into the User module.

Functions that deal with converting between two types – pretty common in functional programming – would generally go into the module of the type being passed in – userToFriend(user, friendData) would go into the User module. In Elixir it would be idiomatic to call that User.to_friend, and if you’re okay with using wildcard imports, that’ll work great:

On the other hand, if you’re following the currently popular JavaScript practice of doing individual imports, then calling the function userToFriend would be more clear:

Consider wildcard imports

However, I think that with this functional module pattern, wildcard imports make a lot of sense. They let you prefix your functions with the type they’re working on, and push you to think of the collection of User-related types and functions as one thing like a class.

But if you do that and declare types, one issue is that then in other classes you’d be referring to the type User.User or User.userType. Yuck. There’s another idiom we can borrow from Elixir here – when declaring types in that language, it’s idiomatic to name the module struct’s type t.

We can replicate that with React PropTypes by just naming the propType t, like so:

It also works just fine in TypeScript, and it’s nice and readable. You use t to describe the type of the current module, and Module.t to describe the type from Module.

Using t in TypesScript does break a popular rule from the TypeScript Coding Guidelines to “use PascalCase for type names.” You could name the type T instead, but then that would conflict with the common TypeScript practice of naming generic types T. Overall, User.t seems like a nice compromise, and the lowercase t feels like it keeps the focus on the module name, which is the real name of the type anyway. This is one for your team to decide on, though.

Wrapping up

Decoupling your business logic from your framework keeps it nicely organized and testable, makes it easier to onboard developers who don’t know your specific framework, and means you don’t have to be thinking about controllers or reducers when you just want to be thinking about users and passwords.

This process doesn’t have to happen all at once. Try pulling all the logic for just one module together, and see how it goes. You may be surprised at how much duplication you find!

So in summary:

Try organizing your functional code by putting functions in the same modules as the types they work on.
Put the module’s data parameter in a consistent position in your function signatures.
Consider using import * as Module wildcard imports, and naming the main module type t.