Encapsulation in Functional Programming by Mark Seemann
Encapsulation is only relevant for object-oriented programming, right?
The concept of encapsulation is closely related to object-oriented programming (OOP), and you rarely hear the word in discussions about (statically-typed) functional programming (FP). I will argue, however, that the notion is relevant in FP as well. Typically, it just appears with a different catchphrase.
Contracts #
I base my understanding of encapsulation on Object-Oriented Software Construction. I've tried to distil it in my Pluralsight course Encapsulation and SOLID.
In short, encapsulation denotes the distinction between an object's contract and its implementation. An object should fulfil its contract in such a way that client code doesn't need to know about its implementation.
Contracts, according to Bertrand Meyer, describe three properties of objects:
- Preconditions: What client code must fulfil in order to successfully interact with the object.
- Invariants: Statements about the object that are always true.
- Postconditions: Statements that are guaranteed to be true after a successful interaction between client code and object.
You can replace object with value and I'd argue that the same concerns are relevant in FP.
In OOP invariants often point to the properties of an object that are guaranteed to remain even in the face of state mutation. As you change the state of an object, the object should guarantee that its state remains valid. These are the properties (i.e. qualities, traits, attributes) that don't vary - i.e. are invariant.
An example would be helpful around here.
Table mutation #
Consider an object that models a table in a restaurant. You may, for example, be working on the Maître d' kata. In short, you may decide to model a table as being one of two kinds: Standard tables and communal tables. You can reserve seats at communal tables, but you still share the table with other people.
You may decide to model the problem in such a way that when you reserve the table, you change the state of the object. You may decide to describe the contract of Table
objects like this:
- Preconditions
- To create a
Table
object, you must supply a type (standard or communal). - To create a
Table
object, you must supply the size of the table, which is a measure of its capacity; i.e. how many people can sit at it. - The capacity must be a natural number. One (1) is the smallest valid capacity.
- When reserving a table, you must supply a valid reservation.
- When reserving a table, the reservation quantity must be less than or equal to the table's remaining capacity.
- To create a
- Invariants
- The table capacity doesn't change.
- The table type doesn't change.
- The number of remaining seats is never negative.
- The number of remaining seats is never greater than the table's capacity.
- Postconditions
- After reserving a table, the number of remaining seats can't be greater than the previous number of remaining seats minus the reservation quantity.
This list may be incomplete, and if you add more operations, you may have to elaborate on what that means to the contract.
In C# you may implement a Table
class like this:
public sealed class Table { private readonly List<Reservation> reservations; public Table(int capacity, TableType type) { if (capacity < 1) throw new ArgumentOutOfRangeException( nameof(capacity), $"Capacity must be greater than zero, but was: {capacity}."); reservations = new List<Reservation>(); Capacity = capacity; Type = type; RemaingSeats = capacity; } public int Capacity { get; } public TableType Type { get; } public int RemaingSeats { get; private set; } public void Reserve(Reservation reservation) { if (RemaingSeats < reservation.Quantity) throw new InvalidOperationException( "The table has no remaining seats."); if (Type == TableType.Communal) RemaingSeats -= reservation.Quantity; else RemaingSeats = 0; reservations.Add(reservation); } }
This class has good encapsulation because it makes sure to fulfil the contract. You can't put it in an invalid state.
Immutable Table #
Notice that two of the invariants for the above Table
class is that the table can't change type or capacity. While OOP often revolves around state mutation, it seems reasonable that some data is immutable. A table doesn't all of a sudden change size.
In FP data is immutable. Data doesn't change. Thus, data has that invariant property.
If you consider the above contract, it still applies to FP. The specifics change, though. You'll no longer be dealing with Table
objects, but rather Table
data, and to make reservations, you call a function that returns a new Table
value.
In F# you could model a Table
like this:
type Table = private Standard of int * Reservation list | Communal of int * Reservation list module Table = let standard capacity = if 0 < capacity then Some (Standard (capacity, [])) else None let communal capacity = if 0 < capacity then Some (Communal (capacity, [])) else None let remainingSeats = function | Standard (capacity, []) -> capacity | Standard _ -> 0 | Communal (capacity, rs) -> capacity - List.sumBy (fun r -> r.Quantity) rs let reserve r t = match t with | Standard (capacity, []) when r.Quantity <= remainingSeats t -> Some (Standard (capacity, [r])) | Communal (capacity, rs) when r.Quantity <= remainingSeats t -> Some (Communal (capacity, r :: rs)) | _ -> None
While you'll often hear fsharpers say that one should make illegal states unrepresentable, in practice you often have to rely on predicative data to enforce contracts. I've done this here by making the Table
cases private
. Code outside the module can't directly create Table
data. Instead, it'll have to use one of two functions: Table.standard
or Table.communal
. These are functions that return Table option
values.
That's the idiomatic way to model predicative data in statically typed FP. In Haskell such functions are called smart constructors.
Statically typed FP typically use Maybe (Option
) or Either (Result
) values to communicate failure, rather than throwing exceptions, but apart from that a smart constructor is just an object constructor.
The above F# Table
API implements the same contract as the OOP version.
If you want to see a more elaborate example of modelling table and reservations in F#, see An F# implementation of the Maître d' kata.
Functional contracts in OOP languages #
You can adopt many FP concepts in OOP languages. My book Code That Fits in Your Head contains sample code in C# that implements an online restaurant reservation system. It includes a Table
class that, at first glance, looks like the above C# class.
While it has the same contract, the book's Table
class is implemented with the FP design principles in mind. Thus, it's an immutable class with this API:
public sealed class Table { public static Table Standard(int seats) public static Table Communal(int seats) public int Capacity { get; } public int RemainingSeats { get; } public Table Reserve(Reservation reservation) public T Accept<T>(ITableVisitor<T> visitor) public override bool Equals(object? obj) public override int GetHashCode() }
Notice that the Reserve
method returns a Table
object. That's the table with the reservation associated. The original Table
instance remains unchanged.
The entire book is written in the Functional Core, Imperative Shell architecture, so all domain models are immutable objects with pure functions as methods.
The objects still have contracts. They have proper encapsulation.
Conclusion #
Functional programmers may not use the term encapsulation much, but that doesn't mean that they don't share that kind of concern. They often throw around the phrase make illegal states unrepresentable or talk about smart constructors or partial versus total functions. It's clear that they care about data modelling that prevents mistakes.
The object-oriented notion of encapsulation is ultimately about separating the affordances of an API from its implementation details. An object's contract is an abstract description of the properties (i.e. qualities, traits, or attributes) of the object.
Functional programmers care so much about the properties of data and functions that property-based testing is often the preferred way to perform automated testing.
Perhaps you can find a functional programmer who might be slightly offended if you suggest that he or she should consider encapsulation. If so, suggest instead that he or she considers the properties of functions and data.
Comments
I wonder what's the goal of illustrating OOP-ish examples exclusively in C# and FP-ish ones in F# when you could stick to just one language for the reader. It might not always be as effective depending on the topic, but for encapsulation and the examples shown in this article, a C# version would read just as effective as an F# one. I mean when you get round to making your points in the Immutable Table section of your article, you could demonstrate the ideas with a C# version that's nearly identical to and reads as succinct as the F# version:
This way, I can just point someone to your article for enlightenment, 😉 but not leave them feeling frustrated that they need F# to (practice and) model around data instead of state mutating objects. It might still be worthwhile to show an F# version to draw the similarities and also call out some differences; like
Table
being a true discriminated union in F#, and while it appears to be emulated in C#, they desugar to the same thing in terms of CLR types and hierarchies.By the way, in the C# example above, I modeled the standard table variant differently because if it can hold only one reservation at a time then the model should reflect that.
Atif, thank you for supplying and example of an immutable C# implementation.
I already have an example of an immutable, functional C# implementation in Code That Fits in Your Head, so I wanted to supply something else here. I also tend to find it interesting to compare how to model similar ideas in different languages, and it felt natural to supply an F# example to show how a 'natural' FP implementation might look.
Your point is valid, though, so I'm not insisting that this was the right decision.
I took your idea, Atif, and wrote something that I think is more congruent with the example here. In short, I’m
Here's the code:
I’d love to hear your thoughts on this approach. I think that one of its weaknesses is that calls to
Table.Standard()
andTable.Communal()
will yield two instances ofTable
that can never be equal. For instance,Table.Standard(4) != Table.Communal(4)
, even though they’re both of typeTable?
and have the same number of seats.Calling
GetType()
on each of the instances reveals that their types are actuallyTable+StandardTable
andTable+CommunalTable
respectively; however, this isn't transparent to callers. Another solution might be to expose theTable
subtypes and give them private constructors – I just like the simplicity of not exposing the individual types of tables the same way you’re doing here, Mark.Mark,
How do you differentiate encapsulation from abstraction?
Here's an excerpt from your book Dependency Injection: Principles, Practices, and Patterns.
Section: 1.3 - What to inject and what not to inject Subsection: 1.3.1 - Stable Dependencies
In that section, you and Steven were giving examples of stable dependencies that do not require to be injected to keep modularity. You define a library that "encapsulates an algorithm" as an example.
Now, to me, encapsulation is "protecting data integrity", plain and simple. A class is encapsulated as long as it's impossible or nearly impossible to bring it to an invalid or inconsistent state.
Protection of invariants, implementation hiding, bundling data and operations together, pre- and postconditions, Postel's Law all come into play to achieve this goal.
Thus, a class, to be "encapsulatable", has to have a state that can be initialized and/or modified by the client code.
Now I ask: most of the time when we say that something is encapsulating another, don't we really mean abstracting?
Why is it relevant to know that the hypothetical algorithm library protects it's invariants by using the term "encapsulate"?
Abstraction, under the light of Robert C. Martin's definition of it, makes much more sense in that context: "a specialized library that abstracts algorithms relevant to your application". It amplifies the essential (by providing a clear API), but eliminates the irrelevant (by hiding the alogirthm's implementation details).
Granted, there is some overlap between encapsulation and abstraction, specially when you bundle data and operations together (rich domain models), but they are not the same thing, you just use one to achieve another sometimes.
Would it be correct to say that the .NET Framework encapsulates math algorithms in the System.Math class? Is there any state there to be preserved? They're all static methods and constants. On the other hand, they're surely eliminating some pretty irrelevant (from a consumer POV) trigonometric algorithms.
Thanks.
Alexandre, thank you for writing. How do I distinguish between abstraction and encapsulation?
There's much overlap, to be sure.
As I write, my view on encapsulation is influenced by Bertrand Meyer's notion of contract. Likewise, I do use Robert C. Martin's notion of amplifying the essentials while hiding the irrelevant details as a guiding light when discussing abstraction.
While these concepts may seem synonymous, they're not quite the same. I can't say that I've spent too much time considering how these two words relate, but shooting from the hip I think that abstraction is a wider concept.
You don't need to read much of Robert C. Martin before he'll tell you that the Dependency Inversion Principle is an important part of abstraction:
It's possible to implement a code base where this isn't true, even if classes have good encapsulation. You could imagine a domain model that depends on database details like a particular ORM. I've seen plenty of those in my career, although I grant that most of them have had poor encapsulation as well. It is not, however, impossible to imagine such a system with good encapsulation, but suboptimal abstraction.
Does it go the other way as well? Can we have good abstraction, but poor encapsulation?
An example doesn't come immediately to mind, but as I wrote, it's not an ontology that I've given much thought.