1

Encapsulation in Functional Programming

 1 year ago
source link: https://blog.ploeh.dk/2022/10/24/encapsulation-in-functional-programming/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK