OOP vs FP – Is Semantic Contract of Interface More Informative Than Function Signature?

functional programmingobject-orientedsolid

It is said by some that if you take SOLID principles to their extremes, you end up at functional programming. I agree with this article but I think that some semantics are lost in the transition from interface/object to function/closure, and I want to know how Functional Programming can mitigate the loss.

From the article:

Furthermore, if you rigorously apply the Interface Segregation Principle (ISP), you'll understand that you should favour Role Interfaces over Header Interfaces.

If you keep driving your design towards smaller and smaller interfaces, you'll eventually arrive at the ultimate Role Interface: an interface with a single method. This happens to me a lot. Here's an example:

public interface IMessageQuery
{
    string Read(int id);
}

If I take a dependency on an IMessageQuery, part of the implicit contract is that calling Read(id) will search for and return a message with the given ID.

Compare this to taking a dependency on its equivalent functional signature, int -> string. Without any additional cues, this function could be a simple ToString(). If you implemented IMessageQuery.Read(int id) with a ToString() I may accuse you of being deliberately subversive!

So, what can functional programmers do to preserve the semantics of a well-named interface? Is it conventional to, for example, create a record type with a single member?

type MessageQuery = {
    Read: int -> string
}

Best Answer

As Telastyn says, comparing the static definitions of functions:

public string Read(int id) { /*...*/ }

to

let read (id:int) = //...

You haven't really lost anything going from OOP to FP.

However, this is only part of the story, because functions and interfaces aren't only referred to in their static definitions. They're also passed around. So let's say our MessageQuery was read by another piece of code, a MessageProcessor. Then we have:

public void ProcessMessage(int messageId, IMessageQuery messageReader) { /*...*/ }

Now we can't directly see the method name IMessageQuery.Read or its parameter int id, but we can get there very easily through our IDE. More generally, the fact that we're passing an IMessageQuery rather than just any interface with a method a function from int to string means we're keeping that id parameter name metadata associated with this function.

On the other hand, for our functional version we have:

let read (id:int) (messageReader : int -> string) = // ...

So what have we kept and lost? Well, we still have the parameter name messageReader, which probably makes the type name (the equivalent to IMessageQuery) unnecessary. But now we've lost the parameter name id in our function.


There's two main ways around this:

  • Firstly, from reading that signature, you can already make a pretty good guess what's going to be going on. By keeping functions short, simple and cohesive and using good naming, you make it a lot easier to intuit or find this information. Once we got into reading the actual function itself, it'd be even simpler.

  • Secondly, it's considered idiomatic design in many functional languages to create small types to wrap primitives. In this case, the opposite is happening- instead of replacing a type name with a parameter name (IMessageQuery to messageReader) we can replace a parameter name with a type name. For example, int could be wrapped in a type called Id:

    type Id = Id of int
    

    Now our read signature becomes:

    let read (id:int) (messageReader : Id -> string) = // ...
    

    Which is just as informative as what we had before.

    As a side note, this also provides us some of the compiler protection we had in OOP. Whereas the OOP version ensured we took specifically a IMessageQuery rather than just any old int -> string function, here we have a similar (but different) protection that we're taking an Id -> string rather than just any old int -> string.


I'd be reluctant to say with 100% confidence that these techniques will always be just as good and informative as having the full information available on an interface, but I think from the above examples, you can say that most of the time, we can probably do just as good a job.

Related Topic