译 文 : A Proposal: Elixir-style Modules in JavaScript by Will Ockelmann-Wagner 13th August 2018

On the original

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?

Switching code to a functional style has many benefits — it’s easier to find causes, easier to test, more declarative, and so on. But sometimes this can have a very bad effect on one aspect of your code: the “organization” of your code. By comparison, we found that classes in object-oriented programming are a good unit of code organization — methods must be placed in the same class as their associated data so that your code becomes logically organized.

However, even in modern JavaScript projects, things are often not that clear cut. If you’re building your application around the structure of your architecture — components, services, controllers — the code in these architectures is mostly stateful classes with lots of dependencies. As a good functional programmer, you split the business logic into smaller functions and then weave it together in your component to do some state transition work. But then you can test them individually, and the world is very harmonious.

But the question is how do you place these little functions?

General practice

On the original

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.

The first answer is usually “at the bottom of the file”. Let’s say you now have a component called userComponent.js. You can imagine a pair of purely auxiliary functions at the bottom of the file — such as fullName(user) — and exporting them to test in the userComponent.spec.js file.

But over time, you add a few more functions. At this point, the component is several months old, the file is over 300 lines long, and it no longer looks like a component, but more like a stack of pure functions. Obviously, this is the time to start separating them. So, now that you have a UserComponent, why not put these functions in a separate UserComponenthelpers.js file? Your component should be clean, just import the required functions from the helper file.

On the original

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.

So far so good — even if the UserComponenthelpers.js file is like a function grocery bag — fullName(user) is pasted with formatDate(date).

Then you have a new requirement to display the user’s full name in the navigation bar. Ok, now you need to use the “fullName” function in two places. So, you throw it in an utils file? That’s not good!

On the original

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.

Then a few months later, you’re looking at the FriendsComponent and you see that someone has implemented fullName here. Embarrassed! The next time you need a user-specific function, check to see if you already have one. At least UserComponent, UserComponentHelpers, FriendsComponents, There are also UserApiServeice — the file that does some of the conversion of the User object — these files.

Modules in Elixir

On the original

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:

There is a concept in Elixir called a struct, which is a dictionary data structure with predefined attributes. This is not unique to Elixir, but it develops it into a very useful form. There is usually only one module per file, and each module can put some functions or define a struct. So, a module named User would normally look like this:

defmodule User do
  defstruct [:first_name.:last_name.:email]

  def full_name(user = %User{}) do
    "#{user.first_name} #{user.last_name}
  end
end
Copy the code
On the original

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_namefunction 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.

The above code — even if you’ve never seen Elixir before — is pretty easy to read. A User structure is defined to contain first_name, last name, and email; A related function, full_name, takes a User and operates on it. This module is organized like a class — we can define the data that makes up a User and the operation logic associated with him in one place, without the “mutable state” problem.

Modules in JavaScript

On the original

There’s no reason we can’t use the same pattern in javascript-land. Instead of organizing your pure functions around the The components they ‘re 2 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:

This can be done in other languages, and there’s no reason why it can’t be done in JavaScript. Instead of organizing your pure functions around the components they use, you can instead organize your code around data types (the term in Domain-driven design is domain objects).

So, you can take all the user-specific pure functions out of all the discrete components and put them in a single user.js file. This is useful, but (in addition to pure functions) both the class and Elixir modules have their own data structures (User structures/classes) defined — including logic.

JavaScript has no built-in way to do this, but there is the simplest solution that can be achieved by adding a few comments. That’s JSDoc — a very popular set of specifications for writing “machine-readable” comment documents that let you define a type via the @typedef tag:

/** * @typedef {Object} User * @property {string} firstName * @property {string} lastName * @property {string} email */

/** * @param {User} user * @returns {string} */
export function fullName(user) {
  return `${user.firstName} ${user.lastName}`;
}
Copy the code
On the original

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 will allow us to migrate all the information in the Elixir module to JavaScript, which will help other developers in the future understand what User objects look like on your system. The problem with comments, however, is that they are out of date. This is where languages like TypeScript come in handy. With TypeScript, you can take advantage of a defined interface that the compiler ensures is “never expired.”

export interface User {
  firstName: string;
  lastName: string;
  email: string;
}

export function fullName(user: User) :string {
  return `${user.firstName} ${user.lastName}`;
}
Copy the code
On the original

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.

The propTypes in React have the same effect. Proptypes are just objects that can be exported, so you can define the PropType of your User in your module via proptyp.Shape.

export const userType = PropTypes.shape({
  firstName: PropTypes.string;
  lastName: PropTypes.string;
  email: PropTypes.string;
});

export function fullName(user) {
  return `${user.firstName} ${user.lastName}`;
}
Copy the code

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

You can then use the User type in your components, reducers, and selectors.

:

  1. Reducer: array.prototype. reduce Method callback function is often called reducer;

  2. Selector is a callback function of the filter, every, find, and other judgment methods of an exponential group.

import React from'the react;import {userType, fullName} from'/ user';const UserComponent = user= > (
  <div>Name: {fullName(user)}</div>
);
UserComponent.propTypes = {
  user: userType
};
Copy the code
On the original

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.

You can do something like Facebook’s Flow, or any other library that lets you define data contours.

When defining your data type, the key is to put the definition of the data type and the logic associated with it together. This makes it clear what your functions do and what data they operate on. In addition, since all user-related logic is in one place, you will find some of the same logic scattered in different corners of the code base that is not easy to detect and migrate it in.

Position of parameter

On the original

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:

It’s a good practice to always place data types in modules in a specific position in your function’s entry list – either always on the first argument or always on the last argument – if you need to curry a lot. Either option is good and will help you locate the argument — if putting user in the primary position is weird, it means the function shouldn’t be in the User module at all.

Functions that handle conversions between two data types — very common in functional programming — are usually placed in the same module as the data type being passed in, such as userToFriend(user, friendData) in the User module. In Elixir, it is customary to call user.to_friend, which is fine if you feel comfortable using wildcard imports:

import * as User from 'accounts/User';

User.toFriend(user):
Copy the code

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:

However, if you follow the popular JavaScript practice of “decentralised import”, calling userToFriend is much clearer:

import { userToFriend } from 'accounts/User';

userToFriend(user):
Copy the code

Thinking about the form of wildcard import

On the original

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 variable 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:

But I think it makes more sense to import wildcards in this functional module pattern. Because it lets you prefix a function with a type that represents its purpose, it forces you to think of user-related data types and functions as a whole — as if it were a class.

But if you do, there is a problem: in other classes, you need to refer to the class as user. User or user.userType. This is really annoying! But we can borrow a “custom” from Elixir — when you declare a type in this language, it is a convention to name the structure of the module T.

In the React PropType, we can achieve the same effect by naming the PropType t, like this:

export const t = PropTypes.shape({
  firstName: PropTypes.string;
  lastName: PropTypes.string;
  email: PropTypes.string;
});

export function fullName(user) {
  return `${user.firstName} ${user.lastName}`;
}
Copy the code
import React from'the react;import * as User from'/ user';const UserComponent = user= > (
  <div>Name: {User.fullName(user)}</div>
);
UserComponent.propTypes = {
  user: User.t
};
Copy the code

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.

This also works in TypeScript, and is much better and more readable. Use t to indicate the type of the current module; Use module. t to represent types in modules.

export interface t {
  firstName: string;
  lastName: string;
  email: string;
}

export function fullName(user: t) :string {
  return `${user.firstName} ${user.lastName}`;
}
Copy the code
import * as User from './user';

class UserComponent {
  name(): User.t {
    return User.fullName(this.user); }}Copy the code
On the original

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.tseems 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.

Using T in TypeScript breaks one of the more popular principles advocated in the TypeScript code guidelines — PascalCase naming conventions in type names. You could name it T, but that would conflict with TypeScript’s common practice of naming generics T. To sum up, user.t seems like a good compromise; the lowercase T makes it look like it describes the name of the module, but it’s really the name of the type. Anyway, it’s up to your team to decide.

conclusion

On the original

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!

Decoupling the business logic from your architecture can effectively maintain the organization and testability of the code, making it easy for developers who are not familiar with your architecture to get started. It also means that when you only consider users and passwords, you do not have to think about controllers and reducer.

This process doesn’t have to be done all at once. Try to gather all the related logic from just one module, and then watch it change. You’ll be surprised at how much repetition there is in your code.

On the original

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.

In short:

  • Try to put your functional code in the same module as its associated data type.
  • Place the module’s data parameters in fixed locations for each function.
  • Consider aimport * as ModuleForm wildcard import and name the module’s primary data type ast.