anchor
Insights /  
Comprehensive Review of Clojure Protocols

Clojure Protocols and the Path through the Expression Problem

April 18, 2024
->
6 min read
Clojure
By
Oleksandr Druk
Clojure Developer
Sofiia Yurkevska
Content Writer

As your business grows, your software systems must adapt and expand to meet new requirements. Whether you add new product offerings, expand into new markets, or integrate with new partners and vendors, your technology must evolve alongside your business. The challenge is that changing your software's core parts can be risky, time-consuming and costly. We believe that protocols in Clojure are just the feature for the task. Let’s see why.

Understanding Clojure Protocols

So what are those? Protocols in Clojure describe a set of functions a data type must implement to be used polymorphically. Polymorphism, in turn, enables us to create reusable, extensible, and easy-to-maintain code at higher abstraction levels that can work with various data types rather than having to write separate implementations for each type.

Clojure Protocols define a standard "contract" that various data types can adhere to, allowing you to consistently create business logic that can work with those data types. Pretty similar to interfaces in OOP languages like Java, except protocols can be extended to types that don’t declare it.

Protocols are crucial for building adaptability and extensibility into your applications from the ground up. They allow you to seamlessly add new data types and functionality to your software without reworking or modifying your existing codebase. Rather than being limited to a rigid set of predefined features, protocols enable you to extend your applications in a modular, scalable way. New capabilities can be added in isolated, well-encapsulated namespaces or packages without disrupting the rest of your system.

Breaking Down the Expression Problem

As our programs grow, we must provide new data types and operations that extend the ones we already have. We want this to be a true extension, not just modifying already written code but respecting the existing abstractions. We likely want these extensions to happen in a separate namespace or package without breaking everything. 

This is where the so-called Expression Problem comes into play. The expression problem describes the challenge of adding new features, products, or data types to your software without reworking or rewriting your existing codebase.

Imagine you've built a successful e-commerce platform for selling widgets. But now your business is expanding into a new product line – gadgets. Ideally, you'd want to add support for the latest gadget product type to your platform without modifying the underlying code heavily designed for widgets.

This is the essence of the Expression problem. As your business requirements change and expand, your technology must grow and adapt in lockstep – without major reworks' risk, cost, and disruption. Failing to address the Expression problem can leave your software systems inflexible and unable to keep pace with your evolving business needs. You end up trapped in a cycle of costly, time-consuming rewrites just to add new functionality.

The Benefits of Protocols

Adaptability
Protocols allow you to easily add new functionality and data types to your application without reworking the codebase.
Scalability
By decoupling your logic from specific data implementations, protocols make it easier to scale your application as your business grows.
Reduced maintainance costs
Protocols promote modular, extencible code that is simpler to maintain and update over time, saving your development team time and money.
Future-proofing
With protocols, you can future-proof your applications to integrate them faster with new systems, partners, or market changes.

Declaration of protocols

That was a theory, so let's get our hands dirty and define a simple protocol:

1(defprotocol Length
2    "A doc string for Length protocol"
3    (length [this] "A doc string for length method"))

We defined the Length protocol with a single method called length, which has been pretty easy so far. But what happens under the hood?

  • a Var Length will be defonce-ed in the namespace, 
  • a Clojure immutable map with protocol information will be constructed,
  • a Java interface with Protocol methods and their signatures will be dynamically generated and loaded into the classloader,
  • the protocol map mentioned above is alter-var-root-ed to be the value of the Length var.

Let's extend a few Clojure built-in types with our protocol. We can use extend, which is pretty low-level for this. Still, usually, we'll be using extend-type if we want to extend a given type or class with one or more protocols or extend-protocol if we're going to extend one or more types or classes with a given protocol. extend-type and extend-protocol are macroses and expand in many extend calls under the hood. So we can write something like this:

1(extend-type String Length
2	(length [this] (.length this)))
3(length "12345") ; => 5
4(extend-type Long Length
5	(length [this] (length (str this))))
6(length 12345) ; => 5

or like this:

1(extend-protocol Length
2	String (length [this] (.length this))
3	Long (length [this] (length (str this))))
4
5(length "12345") ; => 5
6(length 12345) ; => 5
To define a default protocol implementation, we can extend it for java.lang.Object, as every data type in Clojure is derived from it.

1(extend-protocol Length
2	java.lang.Object (length [this] (str "Default Length implementation for " (type this))))
3(length {}) ; => "Default Length implementation for class clojure.lang.PersistentArrayMap"
4(length [1 2 3]) ; => "Default Length implementation for class clojure.lang.PersistentVector"

Protocols vs. Multimethods

Clojure offers two primary tools for solving the expression problem and building adaptable software: protocols and multimethods. You might wonder, "If multimethods can solve the expression problem, why do I also need protocols?"

Protocols offer a critical advantage by allowing you to group related functions into cohesive abstractions. Rather than having independent, scattered multimethods, protocols will enable you to encapsulate all the necessary functionality for working with a particular data type or feature set.

This grouping of functions under a protocol provides two essential benefits for businesses:

Improved maintainability
Organizing your code around well-defined protocol "contract" makes it easier to comprehend, navigate and update your software system over time. This reduces the ongoing cost and complexity of managing your technology stack.
Enhanced performance
Clojure is designed to run on top of other languages platforms, like Java Virtual Machine. These underlying platforms often have specialized optimizations for dispatching method calls based on the first argument type. Protocols leverage these platform-level optimizations, making them faster and more efficient than the more general-purpose dispatch mechanisms used by multimethods.

In essence, protocols give you the best of both worlds: the flexibility to extend your software in new directions (solving the expression problem) and the performance benefits of tightly integrated, type-based dispatching. This allows you to build applications that are not only highly adaptable but also scalable and cost-effective to maintain.

For businesses seeking to future-proof their technology investments, protocols' advantages over multimethods can be game-changers. By promoting modularity, maintainability, and raw performance, protocols empower you to keep pace with evolving business requirements without sacrificing the integrity and efficiency of your underlying software systems.

Takeaway

Protocols introduce polymorphism, enabling you to seamlessly add new data types, features, and functionality to your Clojure-based application without reworking or compromising the existing codebase. The expression problem is a common challenge for growing businesses, and protocols provide an elegant solution. They give you the freedom to extend types or classes written, for example, in other packages imported into your project as a dependency without fear of breaking everything.

Clojure's hosting on platforms like the JVM allows protocols to take advantage of specialized dispatch optimizations. This makes protocol-based code faster and more efficient than more general-purpose polymorphism mechanisms.

Protocols empower you to build applications that evolve and scale alongside your business. By decoupling your core logic from specific data and feature implementations, you can more easily adapt to changing market demands, integrate with new partners, and expand into new product areas. If you'd like to discuss how Clojure can future-proof your technology investments, we’ll be happy to have a talk.

Author
linkedin
Oleksandr Druk
Clojure Developer

Self-taught developer. Programming languages design enthusiast. 3 years of experience with Clojure.

linkedin
Sofiia Yurkevska
Content Writer

Infodumper, storyteller and linguist in love with programming - what a mixture for your guide to the technology landscape!

Shall we discuss
your idea?
Uploading...
fileuploaded.jpg
Upload failed. Max size for files is 10 MB.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
What happens after you fill this form?
We review your inquiry and respond within 24 hours
We hold a discovery call to discuss your needs
We map the delivery flow and manage the paperwork
You receive a tailored budget and timeline estimation
Looking for a Trusted Outsourcing Partner?