DEV Community

Christos Dimitroulas
Christos Dimitroulas

Posted on

Currying, Part 2 - Partial Application

Introduction

Welcome back to the "Currying - What's the point" series. Last time we looked at how currying can improve function composition. In today's post, we will be looking at a technique called partial application, which is closely related to curried functions.

Partial application is where we pass only some of a function's arguments to the function and in return we get back a function taking the rest of the remaining arguments. For more details on the basics of partial application, take a look at this post. As I did in the previous post, rather than focusing on explaining what the technique is, I will instead be focusing on code examples which showcase how partial application is beneficial to real world applications.

We're going to look at two examples. Firstly, we'll explore a "classical" use-case for partial application which is for dependency injection. Next, we'll take a look at an example of how partial application can be used to handle user permissions.

Dependency Injection

Most developers are familiar with the concept of dependency injection (DI). Usually, big frameworks with built-in dependency injection containers come to mind when we hear the words "dependency injection", but the concept is actually very simple and doesn't require any of this more advanced machinery to work.

In OOP, DI is generally achieved by accepting arguments in the contructor. For example, we may have a Car class which accepts an instance of ILogger:

class Car {
  private logger: ILogger;

  constructor(logger: ILogger) {
    this.logger = logger;
  }

  honkHorn(): void {
    this.logger.log("HONK HONK!");
  }
}

const logger = { log: console.log }

const car = new Car(logger);
car.honkHorn();
Enter fullscreen mode Exit fullscreen mode

In functional programming, DI is simply achieved by passing arguments to functions! Here's the same honkHorn function written without a class:

const honkHorn = (logger: ILogger) => () =>
  logger.log("HONK HONK!");

const logger = { log: console.log };
honkHorn(logger)();
Enter fullscreen mode Exit fullscreen mode

In both paradigms, the benefits of using DI are the same:

  1. It makes our code more maintainable as we can focus on a single responsibility.
  2. It keeps the code loosely coupled to other parts of the codebase as our code doesn't depend on anything directly. It only depends on an interface.
  3. It helps to keep our code unit testable by making it easy to mock dependencies.

Now that we've gotten the basics out of the way, let's look at a common example that we encounter in web applications. In most web applications, a lot of our work results in an interaction with a database. Since most applications deal with users, we can take a look at how we can use partial application to our advantage when we need to update a user's email.

We have some core logic we want to enforce:

  • The email must be of a valid format
  • After the email is saved, we must log details about the fact that a user changed their email

Let's define a function updateUserEmail which encapsulates this core logic:

export type User = {
  id: number,
  email: string
}

export type ILogger = {
  info: (msg: string) => void
}

export type IUserRepository = {
  fetchUser: (userId: number) => Promise<User>,
  saveUserChanges: (user: User) => Promise<void>
};


type Dependencies = {
  logger: ILogger,
  userRepository: IUserRepository
}

// Very basic implementation, just for illustration
const checkEmailFormat = (email: string): void => {
  if (!email.includes("@")) {
    throw new Error("Not a valid email");
  }
};

export const updateUserEmail = (
  dependencies: Dependencies
) => async (userId: number, newEmail: string): Promise<void> => {
  const { logger, userRepository } = dependencies;

  checkEmailFormat(newEmail);
  const user = await userRepository.fetchUser(userId);
  const updatedUser = { ...user, email: newEmail };
  await userRepository.saveUserChanges(updatedUser);
  logger.info(`Email updated for user with ID ${user.id}`);
}
Enter fullscreen mode Exit fullscreen mode

Since our updateUserEmail function is curried and accepts the dependencies argument first, we can easily partially apply it to get back a function which has the dependencies "baked in". We can then pass this new function to the various places that require it in our app, such as in an Express request handler.

import express from "express";
import {
  ILogger,
  IUserRepository,
  updateUserEmail,
  User
} from "./updateUserEmail";

// Simple logger which logs to the console
const logger: ILogger = {
  info: console.log
};

// A few mock users
const users: Record<string, User> = {
  1: { id: 1, email: "test@example.com" },
  2: { id: 2, email: "test@example.co.uk" }
};

// Simple in-memory implementation for IUserRepository
const userRepository: IUserRepository = {
  fetchUser: async (userId) => {
    const user = users[userId];
    if (!user) {
      throw new Error("User not found");
    }
    return user
  },
  saveUserChanges: async user => {
    users[user.id] = user;
  }
};

// We partially apply updateUserEmail, getting back a function
// with the dependencies baked in
const updateUserEmailWithDeps = updateUserEmail({
  logger,
  userRepository
});

// Now we can use our function where it's needed without
// needing to worry about the dependencies.
// Here's an example of it being used in an express handler
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.post("/users/:id/email", async (req, res) => {
  const email: string = req.body.email;
  const userId: number = parseInt(req.params.id);

  // In a real app we would need to add some error-handling
  // to this :)
  const updatedUser = await updateUserEmailWithDeps(userId, email);

  res.status(200).send(updatedUser);
});

app.listen(8080, () => {
  console.log("App listening on port 8080");
})
Enter fullscreen mode Exit fullscreen mode

There are a few clear benefits here.

  1. Our express request handler code is able to purely focus on handling the wiring between our core logic and HTTP requests and responses.
  2. Our core logic can easily be reused in multiple places.
  3. As mentioned earlier, our core logic is blissfully ignorant of things like HTTP requests and even databases, making it easy to maintain. Click here to see example unit tests for updateUserEmail.

User permissions with partial application

One interesting usage of partial application is for handling user permissions. Let's imagine a system which has two user types: 1. a customer 2. an admin.

Let's extend our user type from earlier to look like this:

type User = {
  id: number,
  email: string,
  type: "customer" | "admin"
}
Enter fullscreen mode Exit fullscreen mode

We can continue thinking about the case where we need to update a user's email, but let's forget about the dependency injection for now. Let's also curry the function some more so that all the arguments can be passed one at a time. The reason for this will become clear as we go through the example.

declare const updateUserEmail: (
  userId: number
) => (email: string) => Promise<User>
Enter fullscreen mode Exit fullscreen mode

Here are the security rules we want to enforce:

  • An admin is allowed to update any users's account.
  • A customer can only update their own account.

We can use partial application to our advantage to enforce these rules.

type User = {
  id: number,
  email: string,
  type: "customer" | "admin"
};

// Create separate AdminUser and CustomerUser types
type AdminUser = User & { type: "admin" };
type CustomerUser = User & { type: "customer" };

type CustomerActions = {
  updateUserEmail: (email: string) => Promise<User>
};

type AdminActions = {
  updateUserEmail: (
    userId: number
  ) => (email: string) => Promise<User>
};

// Overloaded function which had a different return type
// based on the type of user passed in.
function getPermittedActions(user: CustomerUser): CustomerActions;
function getPermittedActions(user: AdminUser): AdminActions;
function getPermittedActions(
  user: User
): CustomerActions | AdminActions {
  switch (user.type) {
    case "customer":
      return {
        // Using partial application, we "bake" in the customer's
        // user ID into the `updateUserEmail` function.
        updateUserEmail: updateUserEmail(user.id)
      };

    case "admin":
      return {
        updateUserEmail
      };
  }
}

declare const admin: AdminUser;
const permittedAdminActions = getPermittedActions(admin);

// Admin can update the email on any account, as we wanted
permittedAdminActions.updateUserEmail(1234)("new-email@test.com");

declare const customer: CustomerUser;
const permittedCustomerActions = getPermittedActions(customer);

// Customer doesn't even have the option of passing a customer ID.
// Their own ID is "baked" in to the function, which prevents any
// security concern.
permittedCustomerActions.updateUserEmail("new-email@test.com");

// The type system prevents us from trying to pass a customer ID
// in the same we did for the admin user:
permittedCustomerActions
  .updateUserEmail(1234)("new-email@test.com"); // Type error!
Enter fullscreen mode Exit fullscreen mode

As you can see, with the use of partial application we can bake in arguments to our functions to aid us in enforcing user permission rules. Having a strict compiler which can help you enforce these rules with static types such as Typescript is also a great help in these scenarios.

Conclusion

Partial application is a simple technique with a wide range of use-cases, the only limit is your creativity and imagination. In this post I have shown a couple of interesting examples which hopefully demonstrate why you may wish to use this technique in your applications moving forwards.

As always, I look forward to your questions, comments and feedback!

Discussion (0)