Object isomorphisms by Mark Seemann
An object is equivalent to a product of functions. Alternative ways to look at objects.
This article is part of a series of articles about software design isomorphisms. So far, you've seen how to represent a single method or function in many different ways, but we haven't looked much at objects (in the object-oriented interpretation of the word).
While this article starts by outlining the abstract concepts involved, an example is included towards the end.
Objects as data with behaviour #
I often use the phrase that objects are data with behaviour. (I'm sure I didn't come up with this myself, but the source of the phrase escapes me.) In languages like C# and Java, objects are described by classes, and these often contain class fields. These fields constitute an instance's data, whereas its methods implement its behaviour.
A class can contain an arbitrary number of fields, just like a method can take an arbitrary number of arguments. As demonstrated by the argument list isomorphisms, you can also represent an arbitrary number of arguments as a Parameter Object. The same argument can be applied to class fields. Instead of n fields, you can add a single 'data class' that holds all of these fields. In F# and Haskell these are called records. You could also dissolve such a record to individual fields. That would be the inverse refactoring, so these representations are isomorphic.
In other words, a class looks like this:
public class Class1 { public Data1 Data { get; set; } public Out1 Op1(In1 arg) { // Do stuff with this, Data, and arg; return an Out1 value. } public Out2 Op2(In2 arg) { // Do stuff with this, Data, and arg; return an Out1 value. } public Out3 Op3(In3 arg) { // Do stuff with this, Data, and arg; return an Out1 value. } // More members... }
Instead of an arbitrary number of fields, I've used the above isomorphism to represent data in a single Data
property (Java developers: a C# property is a class field with public getter and setter methods).
In this code example, I've deliberately kept the naming abstract. The purpose of this article series is to look at the shape of code, instead of what it does, or why. From argument list isomorphisms we know that we can represent any method as taking a single input value, and returning a single output value. The remaining work to be done in this article is to figure out what to do when there's more than a single method.
Module #
From function isomorphisms we know that static methods are isomorphic to instance methods, as long as you include the original object as an extra argument. In this case, all data in Class1
is contained in a single (mutable) Data1
record, so we can eliminate Class1
from the argument list in favour of Data1
:
public static class Class1 { public static Out1 Op1(Data1 data, In1 arg) { // Do stuff with data and arg; return an Out1 value. } public static Out2 Op2(Data1 data, In2 arg) { // Do stuff with data and arg; return an Out1 value. } public static Out3 Op3(Data1 data, In3 arg) { // Do stuff with data and arg; return an Out1 value. } // More members... }
Notice that Class1
is now a static
class. This simply means that it has no instance members, and if you try to add one, the C# compiler will complain.
This is, in essence, a module. In F#, for example, a module
is a static class that contains a collection of values and functions.
Closures as behaviour with data #
As data with behaviour, objects are often passed around as input to methods. It's a convenient way to pass both data and associated behaviour (perhaps even with polymorphic dispatch) as a single thing. You'd be forgiven if you've looked at the above module-style refactoring and found it lacking in that regard.
Nevertheless, function isomorphisms already demonstrated that you can solve this problem with closures. Imagine that you want to package all the static methods of Class1
with a particular Data1
value, and pass that 'package' as a single argument to another method. You can do that by closing over the value:
var data = new Data1 { /* initialize members here */ }; Func<In1, Out1> op1 = arg => Class1.Op1(data, arg); Func<In2, Out2> op2 = arg => Class1.Op2(data, arg); Func<In3, Out3> op3 = arg => Class1.Op3(data, arg); // More closures...
First, you create a Data1
value, and initialise it with your desired values. You then create op1
, op2
, and so on. These are functions that close over data
; A.K.A. closures. Notice that they all close over the same variable. Also keep in mind here that I'm in no way pretending that data
is immutable. That's not a requirement.
Now you have n closures that all close over the same data
. All you need to do is to package them into a single 'object':
var objEq = Tuple.Create(op1, op2, op3 /* more closures... */);
Once again, tuples are workhorses of software design isomorphisms. objEq
is an 'object equivalent' consisting of closures; it's behaviour with data. You can now pass objEq
as an argument to another method, if that's what you need to do.
Isomorphism #
One common variation that I sometimes see is that instead of a tuple of functions, you can create a record of functions. This enables you to give each function a statically enforced name. In the theory of algebraic data types, tuples and records are both product types, so when looking at the shape of code, these are closely related. Records also enable you to preserve the name of each method, so that this mapping from object to record of functions becomes lossless.
The inverse mapping also exists. If you have a record of functions, you can refactor it to a class. You use the name of each record element as a method name, and the arguments and return types to further flesh out the methods.
Example: simplified Turtle #
As an example, consider this (over-)simplified Turtle class:
public class Turtle { public double X { get; private set; } public double Y { get; private set; } public double AngleInDegrees { get; private set; } public Turtle() { } public Turtle(double x, double y, double angleInDegrees) { this.X = x; this.Y = y; this.AngleInDegrees = angleInDegrees; } public void Turn(double angleInDegrees) { this.AngleInDegrees = (this.AngleInDegrees + angleInDegrees) % 360; } public void Move(double distance) { // Convert degrees to radians with 180.0 degrees = 1 pi radian var angleInRadians = this.AngleInDegrees * (Math.PI / 180); this.X = this.X + (distance * Math.Cos(angleInRadians)); this.Y = this.Y + (distance * Math.Sin(angleInRadians)); } }
In order to keep the example simple, the only operations offered by the Turtle
class is Turn
and Move
. With this simplified API, you can create a turtle
object and interact with it:
var turtle = new Turtle(); turtle.Move(2); turtle.Turn(90); turtle.Move(1);
This sequence of operations will leave turtle
as position (2, 1) and an angle of 90°.
Instead of modelling a turtle as an object, you can instead model it as a data structure and a set of (impure) functions:
public class TurtleData { private double x; private double y; private double angleInDegrees; public TurtleData() { } public TurtleData(double x, double y, double angleInDegrees) { this.x = x; this.y = y; this.angleInDegrees = angleInDegrees; } public static void Turn(TurtleData data, double angleInDegrees) { data.angleInDegrees = (data.angleInDegrees + angleInDegrees) % 360; } public static void Move(TurtleData data, double distance) { // Convert degrees to radians with 180.0 degrees = 1 pi radian var angleInRadians = data.angleInDegrees * (Math.PI / 180); data.x = data.x + (distance * Math.Cos(angleInRadians)); data.y = data.y + (distance * Math.Sin(angleInRadians)); } public static double GetX(TurtleData data) { return data.x; } public static double GetY(TurtleData data) { return data.y; } public static double GetAngleInDegrees(TurtleData data) { return data.angleInDegrees; } }
Notice that all five static methods take a TurtleData
value as their first argument, just as the above abstract description suggests. The implementations are almost identical; you simply replace this
with data
. If you're a C# developer, you may be wondering about the accessor functions GetX
, GetY
, and GetAngleInDegrees
. These are, however, the static equivalents to the Turtle
class X
, Y
, and AngleInDegrees
properties. Keep in mind that in C#, a property is nothing but syntactic sugar over one (or two) IL methods (e.g. get_X()
).
You can now create a pentuple (a five-tuple) of closures over those five static methods and a single TurtleData
object. While you can always do that from scratch, it's illuminating to transform a Turtle
into such a tuple, thereby illustrating how that morphism looks:
public Tuple<Action<double>, Action<double>, Func<double>, Func<double>, Func<double>> ToTuple() { var data = new TurtleData(this.X, this.Y, this.AngleInDegrees); Action<double> turn = angle => TurtleData.Turn(data, angle); Action<double> move = distance => TurtleData.Move(data, distance); Func<double> getX = () => TurtleData.GetX(data); Func<double> getY = () => TurtleData.GetY(data); Func<double> getAngle = () => TurtleData.GetAngleInDegrees(data); return Tuple.Create(turn, move, getX, getY, getAngle); }
This ToTuple
method is an instance method on Turtle
(I just held it back from the above code listing, in order to list it here instead). It creates a new TurtleData
object from its current state, and proceeds to close over it five times - each time delegating the closure implementation to the corresponding static method. Finally, it creates a pentuple of those five closures.
You can interact with the pentuple just like it was an object:
var turtle = new Turtle().ToTuple(); turtle.Item2(2); turtle.Item1(90); turtle.Item2(1);
The syntax is essentially the same as before, but clearly, this isn't as readable. You have to remember that Item2
contains the move
closure, Item1
the turn
closure, and so on. Still, since they are all delegates, you can call them as though they are methods.
I'm not trying to convince you that this sort of design is better, or even equivalent, in terms of readability. Clearly, it isn't - at least in C#. The point is, however, that from a perspective of structure, these two models are equivalent. Everything you can do with an object, you can also do with a tuple of closures.
So far, you've seen that you can translate a Turtle
into a tuple of closures, but in order to be an isomorphism, the reverse translation should also be possible.
One way to translate from TurtleData
to Turtle
is with this static method (i.e. function):
public static Turtle ToTurtle(TurtleData data) { return new Turtle(data.x, data.y, data.angleInDegrees); }
Another option for making the pentuple of closures look like an object is to extract an interface from the original Turtle
class:
public interface ITurtle { void Turn(double angleInDegrees); void Move(double distance); double X { get; } double Y { get; } double AngleInDegrees { get; } }
Not only can you let Turtle
implement this interface (public class Turtle : ITurtle
), but you can also define an Adapter:
public class TupleTurtle : ITurtle { private readonly Tuple<Action<double>, Action<double>, Func<double>, Func<double>, Func<double>> imp; public TupleTurtle( Tuple<Action<double>, Action<double>, Func<double>, Func<double>, Func<double>> imp) { this.imp = imp; } public void Turn(double angleInDegrees) { this.imp.Item1(angleInDegrees); } public void Move(double distance) { this.imp.Item2(distance); } public double X { get { return this.imp.Item3(); } } public double Y { get { return this.imp.Item4(); } } public double AngleInDegrees { get { return this.imp.Item5(); } } }
This class simply delegates all its behaviour to the implementing pentuple. It can be used like this with no loss of readability:
var turtle = new TupleTurtle(TurtleData.CreateDefault()); turtle.Move(2); turtle.Turn(90); turtle.Move(1);
This example utilises this creation function:
public static Tuple<Action<double>, Action<double>, Func<double>, Func<double>, Func<double>> CreateDefault() { var data = new TurtleData(); Action<double> turn = angle => TurtleData.Turn(data, angle); Action<double> move = distance => TurtleData.Move(data, distance); Func<double> getX = () => TurtleData.GetX(data); Func<double> getY = () => TurtleData.GetY(data); Func<double> getAngle = () => TurtleData.GetAngleInDegrees(data); return Tuple.Create(turn, move, getX, getY, getAngle); }
This function is almost identical to the above ToTuple
method, and those two could easily be refactored to a single method.
This example demonstrates how an object can also be viewed as a tuple of closures, and that translations exist both ways between those two views.
Conclusion #
To be clear, I'm not trying to convince you that it'd be great if you wrote all of your C# or Java using tuples of closures; it most likely wouldn't be. The point is that a class is isomorphic to a tuple of functions.
From category theory, and particular its application to Haskell, we know quite a bit about the properties of certain functions. Once we start to look at objects as tuples of functions, we may be able to say something about the properties of objects, because category theory also has something to say about the properties of tuples (for example that a tuple of monoids is itself a monoid).
Next: Abstract class isomorphism.