Function isomorphisms by Mark Seemann
Instance methods are isomorphic to functions.
This article is part of a series of articles about software design isomorphisms.
While I have already, in an earlier article, quoted the following parable about Anton, Qc Na, objects, and closures, it's too good a fit to the current topic to pass up, so please pardon the duplication.
The venerable master Qc Na was walking with his student, Anton. Hoping to prompt the master into a discussion, Anton said "Master, I have heard that objects are a very good thing - is this true?" Qc Na looked pityingly at his student and replied, "Foolish pupil - objects are merely a poor man's closures."
Chastised, Anton took his leave from his master and returned to his cell, intent on studying closures. He carefully read the entire "Lambda: The Ultimate..." series of papers and its cousins, and implemented a small Scheme interpreter with a closure-based object system. He learned much, and looked forward to informing his master of his progress.
On his next walk with Qc Na, Anton attempted to impress his master by saying "Master, I have diligently studied the matter, and now understand that objects are truly a poor man's closures." Qc Na responded by hitting Anton with his stick, saying "When will you learn? Closures are a poor man's object." At that moment, Anton became enlightened.
The point is that objects and closures are two ways of looking at a thing. In a nutshell, objects are data with behaviour, whereas closures are behaviour with data. I've already shown an elaborate C# example of this, so in this article, you'll get a slightly more formal treatment of the subject.
Isomorphism #
In object-oriented design, you often bundle operations as methods that belong to objects. These are isomorphic to static methods, because a lossless translation exists. We can call such static methods functions, although they aren't guaranteed to be pure.
In the spirit of Refactoring, we can describe each translation as a refactoring, because that's what it is. I don't think the book contains a specific refactoring that describes how to translate from an instance method to a static method, but we could call it Replace Object with Argument.
Going the other way is, on the other hand, already described, so we can use Refactoring's Move Method.
Replace Object with Argument #
While the concept of refactoring ought to be neutral across paradigms, the original book is about object-oriented programming. In object-oriented programming, objects are the design ideal, so it didn't make much sense to include, in the book, a refactoring that turns an instance method into a static method.
Nevertheless, it's straightforward:
- Add an argument to the instance method. Declare the argument as the type of the hosting class.
- In the method body, change all calls to
this
andbase
to the new argument. - Make the method static.
Baz
method:
public class Foo { public Bar Baz(Qux qux, Corge corge) { // Do stuff, return bar } // Other members... }
You'll first introduce a new Foo foo
argument to Baz
, change the method body to use foo
instead of this
, and then make the method static. The result is this:
public class Foo { public static Bar Baz(Foo foo, Qux qux, Corge corge) { // Do stuff, return bar } // Other members... }
Once you have a static method, you can always move it to another class, if you'd like. This can, however, cause some problems with accessibility. In C#, for example, you'd no longer be able to access private
or protected
members from a method outside the class. You can choose to leave the static method reside on the original class, or you can make the member in question available to more clients (make it internal
or public
).
Move Method #
The book Refactoring doesn't contain a recipe like the above, because the goal of that book is better object-oriented design. It would consider a static method, like the second variation of Baz
above, a code smell named Feature Envy. You have this code smell when it looks as if a method is more interested in one of its arguments than in its host object. In that case, the book suggests using the Move Method refactoring.
The book already describes this refactoring, so I'm not going to repeat it here. Also, there's no sense in showing you the code example, because it's literally the same two code examples as above, only in the opposite order. You start with the static method and end with the instance method.
C# developers are most likely already aware of this relationship between static methods and objects, because you can use the this
keyword in a static method to make it look like an instance method:
public static Bar Baz(this Foo foo, Qux qux, Corge corge)
The addition of this
in front of the Foo foo
argument enables the C# compiler to treat the Baz
method as though it's an instance method on a Foo
object:
var bar = foo.Baz(qux, corge);
This is only syntactic sugar. The method is still compiled as a static method, and if you, as a client developer, wish to use it as a static method, that's still possible.
Functions #
A static method like public static Bar Baz(Foo foo, Qux qux, Corge corge)
looks a lot like a function. If refactored from object-oriented design, that function is likely to be impure, but its shape is function-like.
In C#, for example, you could model it as a variable of a delegate type:
Func<Foo, Qux, Corge, Bar> baz = (foo, qux, corge) => { // Do stuff, return bar };
Here, baz
is a function with the same signature as the above static Baz
method.
Have you ever noticed something odd about the various Func
delegates in C#?
They take the return type as the last type argument, which is contrary to C# syntax, where you have to declare the return type before the method name and argument list. Since C# is the dominant .NET language, that's surprising, and even counter-intuitive.
It does, however, nicely align with an ML language like F#. As we'll see in a future article, the above baz
function translates to an F# function with the type Foo * Qux * Corge -> Bar
, and to Haskell as a function with the type (Foo, Qux, Corge) -> Bar
. Notice that the return type comes last, just like in the C# Func
.
Closures #
...but, you may say, what about data with behaviour? One of the advantages, after all, of objects is that you can associate a particular collection of data with some behaviour. The above Foo
class could have data members, and you may sometimes have a need for passing both data and behaviour around as a single... well... object.
That seems to be much harder with a static Baz
method.
Don't worry, write a closure:
var foo = new Foo { /* initialize members here */ }; Func<Qux, Corge, Bar> baz = (qux, corge) => { // Do stuff with foo, qux, and corge; return bar };
In this variation, baz
closes over foo
. Inside the function body, you can use foo
like you can use qux
and corge
. As I've already covered in an earlier article, the C# compiler compiles this to an IL class, making it even more obvious that objects and closures are two sides of the same coin.
Summary #
Object-oriented instance methods are isomorphic to both static methods and function values. The translations that transforms your code from one to the other are refactorings. Since you can move in both directions without loss of information, these refactorings constitute an isomorphism.
This is another important result about the relationship between object-oriented design and functional programming, because this enables us to reduce any method to a canonical form, in the shape of a function. From a language like Haskell, we know a lot about the relationship between category theory and functional programming. With isomorphisms like the present, we can begin to extend that knowledge to object-oriented design.
Next: Argument list isomorphisms.
Comments
This Isomorphism applies to non-polymorphic Methods. Polymorphic Functions need to be mapped to a Set is static Methods with the same Signature. Is there a functional Construct for this?
Matt, thank you for writing. What makes you write that this isomorphism applies (only, I take it) to non-polymorphic methods. The view here is on the implementation. In C# (and all other statically typed languages that I know that support functions), functions are polymorphic based on signature.
A consumer that depends on a function with the type
Func<Foo, Qux, Corge, Bar>
can interact with any function with that type.