Clash of Styles, Part #1 – Operations Matrix via OOP

Intro

This article is the first from a series in which I’ll present a very practical viewpoint of two fundamentally opposite approaches in program design. I hope this wouldn’t be just yet another comparison between the Object-Oriented and Functional paradigms. I’ll do my best to keep a distance from the typical OOP vs. FP rant. Instead of throwing tons of jargon and explaining how cool immutability and pure functions are, I’ll go for a different comparison strategy supported by very concrete implementations.

The presented material is meant to be language agnostic. However, the examples will be written in C# and F#. Still, every piece of code you see will be very easily convertible to other statically typed languages like Java and Scala. Additionally, the core language constructs that I’ll use have their relatives in most of the Object-Oriented or Functional languages. Therefore, you should be able to grasp the essence even if your preferred languages are quite different.

I think sometimes we forget that after all, our job is to create usable programs that will make someone’s life easier. No matter what fancy programming style raises our dopamine levels, in the end, we need to implement the required behavior. And programming is very much about data and different operations on that data. This means that the behavior of our programs can be, to some extend, represented by a simple “Data X Operations” grid that I call the “Operations Matrix.”

The rows of this matrix will contain the different variants of data, while the columns will include the various operations on that data. If we implement all the cells in that matrix, we’ll have our requirements covered entirely. My goal is to show how, looking at the Operations Matrix for a simple example, Object-Oriented and Functional programming paradigms decompose our programs in precisely the opposite way.

This first part of the series will focus on defining the Operations Matrix itself and how we would implement it following the Object-Oriented principles.

Note: This series of articles is very much influenced by some of the core ideas presented in the excellent three-part course “Programming Languages” on Coursera. I encourage you to read my review and pick up the course yourself.

Additionally, you can review Chapter 6 – “Objects and Data Structures” of the bestseller Clean Code by Uncle Bob. Some of the concepts presented here are also discussed in this great book. By the way, you should read the book anyway if you haven’t already.

The Operations Matrix

Let’s say we have the task of implementing a very simple interpreter for arithmetic expressions. Our interpreter will support only the addition of integer values. The main operation of the interpreter will be, of course, evaluating the expression or, in other words, returning the final integer result. Besides that, it will have the feature of building a string representation of the input expression.

In terms of data and operations, this problem definition contains two main components:

Variants of expressions (or expression types)

There are two types of expressions here. The first one is Integer constants. The second one is an Addition expression that is composed of two other inner expressions for the first and second operands. With these types of data, we can build the following expression tree:

Where the blue nodes are Addition expressions, and the green ones are Integer terminals.

Operations

The operations will basically traverse the expression tree while implementing some specific logic. The evaluation operation, for the current example, should return the final result – 12. The “to string” operation should return the stringified version of the expression – “3 + 4 + 5.”

We can quickly put those data variants and operations in the following Operations Matrix:

All we must do is decide and implement the correct behavior for every cell. We need to know how to convert an integer to a string, how to evaluate an addition expression etc. If you want your program to behave correctly, you need to fill out the whole grid – those four question marks in our case.

But how do you approach filling out the grid? Well, this is where Object-Oriented and Functional styles have different opinions. Which one you take may be quite frankly just a matter of personal taste. There are some cases, though, when one of the styles can lead to a more elegant and simplified solution compared to the other one.

In the next section, I will present the Object-Oriented implementation of our Operations Matrix.

OOP Implementation

OOP, in its essence, is about capsulated entities reacting to some external stimuli. Objects are not passive data containers. They have behavior. And really, the behavior is what matters to the users of the object. You can’t directly interact with the object data; all you have access to is a well defined public interface.

So, what’s the public interface of the objects we’ll construct to represent our interpreter’s expressions?  Those are exactly the Eval() and Stringify() operations. Every expression will implement these operations on itself. An Integer will know how to evaluate itself. An Addition expression will know how to convert itself to a string. And so on. In other words, we will fill out the Operations Matrix “by rows,” as shown in the following picture:

Let’s move on to some actual implementation. For everyone comfortable with OOP, the coding part should be quite intuitive. Let’s first see a UML diagram:

If this doesn’t look familiar, I would suggest taking a look at the Composite and Interpreter Design Patterns for some general context of such recursive definitions in OOP.

From the coding perspective, let’s start by defining the common IExpression interface that our variants will implement:

public interface IExpression
{
    int Eval();
    
    string Stringify();
}

And here’s the definition of out Integer constant data type, called MyInt:

public class MyInt : IExpression
{
    private int Val { get; } 
    
    public MyInt(int val)
    {
        Val = val;
    }

    public int Eval() => Val;
    public string Stringify() => Val.ToString();
}

Nothing surprising here. An Integer just evaluates to itself and returns itself as a string.

The Addition is more interesting:

public class Addition : IExpression
{
    private readonly IExpression _operand1;
    private readonly IExpression _operand2;

    public Addition(IExpression operand1, IExpression operand2)
    {
        _operand1 = operand1;
        _operand2 = operand2;
    }

    public int Eval() => _operand1.Eval() + _operand2.Eval();

    public string Stringify() => $"{_operand1.Stringify()} + {_operand2.Stringify()}";
}

That’s a little more complicated but nothing too fancy. I assume you comprehend how the recursive calls in Eval() and Stringify() work.

If you have difficulties understanding the example, I would suggest to type in the implementation manually and step through the code using the following driver code:

var expression = new Addition(
    new MyInt(3), 
    new Addition(new MyInt(4), new MyInt(5)));

var asString = expression.Stringify();
var result = expression.Eval();

Console.WriteLine($"{asString} = {result}"); // 3 + 4 + 5 = 12

Here we just build the expression (3 + (4 + 5)) and make use of the Stringify() and Eval() methods to output the string representation and the result in the console.

The crucial point here is that every object has an implementation for every operation. This is orthogonal to how Functional Programming approaches the same problem – something that we’ll see in the next article.

Summary

That was the OOP implementation of our Operations Matrix. I can imagine you’re already familiar with most of what you saw in this article. Consider it as a warm-up exercise. In the next part, I’ll retake our matrix and see how we’ll fill out the cells “by columns” using Functional Programming.

I’ll start with an attempt to do the functional implementation with C#. We’ll see how this can feel a little awkward for the language itself. Then we’ll move to F# and utilize the potential of Pattern Matching and the Discriminated Union data type to build an elegant FP solution.

Upcoming in the series are more advanced topics like Visitors, Double Dispatch, Extensibility, and many more, so stay tuned!

Thanks for reading.

Resources

  1. Programming Languages, Part A
  2. Programming Languages, Part B
  3. Programming Languages, Part C
  4. https://fsharpforfunandprofit.com/

Site Footer

Subscribe To My Newsletter

Email address