17

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
}
9
  • 6
    An OOP interface is more like FP typeclass, not a single function.
    – 9000
    Jun 16, 2015 at 14:19
  • 2
    You can have too much of a good thing, applying ISP 'rigorously' to end up with 1 method per interface is just going too far.
    – gbjbaanb
    Jun 16, 2015 at 14:33
  • 3
    @gbjbaanb actually the majority of my interfaces have only one method with many implementations. The more you apply SOLID principles, the more you can see the benefits. But that's off-topic for this question
    – Alex
    Jun 16, 2015 at 14:38
  • 1
    @jk.: Well, in Haskell, it's type classes, in OCaml you either use a module or a functor, in Clojure you use a protocol. In any case, you don't usually limit your interface analogy to a single function.
    – 9000
    Jun 16, 2015 at 16:38
  • 2
    Without any additional clues... maybe it is why the documentation is part of the contract?
    – SJuan76
    Jun 16, 2015 at 20:34

5 Answers 5

9

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.

1
  • 2
    This is my favourite answer - that FP encourages the use of semantic types
    – Alex
    Jun 17, 2015 at 10:33
11

When doing FP I tend to use more specific semantic types.

For example, your method for me would become something like:

read: MessageId -> Message

This communicates quite a lot more than the OO(/java)-style ThingDoer.doThing() style

3
  • 2
    +1. In addition, you can encode far more properties into the type system in typed FP languages like Haskell. If that were a haskell type, I would know that it's not doing any IO at all (and thus likely referencing some in-memory mapping of ids to messages somewhere). In OOP languages, I don't have that information - this function could ring up a database as part of calling it.
    – Jack
    Jun 17, 2015 at 4:04
  • 1
    I'm not exactly clear on why this would communicate more than the Java style. What does read: MessageId -> Message tell you that string MessageReader.GetMessage(int messageId) doesn't? Jun 17, 2015 at 11:15
  • @BenAaronson the signal to noise ratio is better. If it's an OO instance method I have no idea what it's doing under the hood, it could have any kind of dependency that's not expressed. As Jack mentions, when you have stronger types, you have more assurances.
    – Daenyth
    Jun 17, 2015 at 13:54
10

So, what can functional programmers do to preserve the semantics of a well-named interface?

Use well named functions.

IMessageQuery::Read: int -> string simply becomes ReadMessageQuery: int -> string or something similar.

The key thing to note is that names are only contracts in the loosest sense of the word. They only work if you and another programmer infer the same implications from the name, and obey them. Because of this, you can really use any name that communicates that implied behavior. OO and functional programming have their names in slightly different places and in slightly different shapes, but their function is the same.

Is the semantic contract of an interface (OOP) more informative than a function signature (FP)?

Not in this example. As I explained above, a single class/interface with a single function isn't meaningfully more informative than a similarly well named standalone function.

Once you get more than one function/field/property in a class, you can infer more information about them because you can see their relation. It's arguable if that's more informative than standalone functions that take the same/similar parameters or standalone functions organized by namespace or module.

Personally, I don't think that OO is significantly more informative, even in more complex examples.

7
  • 2
    What about parameter names? Jun 16, 2015 at 14:06
  • @BenAaronson - what about them? Both OO and functional languages let you specify parameter names, so that is a push. I'm just using the type signature shorthand here to be consistent with the question. In a real language it would look something like ReadMessageQuery id = <code to go fetch based on id>
    – Telastyn
    Jun 16, 2015 at 14:12
  • 1
    Well if a function takes an interface as a parameter, then calls a method on that interface, the parameter names for the interface method are readily available. If a function takes another function as a parameter, it's not easy to assign parameter names for that passed in function Jun 16, 2015 at 14:18
  • 1
    Yes, @BenAaronson this is one of the pieces of information that is lost in the function signature. For example in let consumer (messageQuery : int -> string) = messageQuery 5, the id parameter is just an int. I guess one argument is that you should be passing an Id, not an int. In fact that would make a decent answer in itself.
    – Alex
    Jun 16, 2015 at 14:28
  • @AlexFoxGill Actually I was just in the process of writing up something along those very lines! Jun 16, 2015 at 14:28
0

I disagree that a single function can't have a 'semantic contract'. Consider these laws for foldr:

foldr f z nil = z
foldr f z (singleton x) = f x z
foldr f z (xn <> ys) = foldr f (foldr f z ys) xn

In what sense is that not semantic, or not a contract? You don't need to define a type for a 'foldrer', especially because foldr is uniquely determined by those laws. You know exactly what it's going to do.

If you want to take in a function of some type, you can do the same thing:

-- The first argument `f` must satisfy for all x, y, z
-- > f x x = true
-- > f x y = true and f y x = true implies x = y
-- > f x y = true and f y z = true implies f x z = true
sort :: forall 'a. (a -> a -> bool.t) -> list.t a -> list.t a;

You only need to name and capture that type if you need the same contract multiple times:

-- Type of functions `f` satisfying, for all x, y, z
-- > f x x = true
-- > f x y = true and f y x = true implies x = y
-- > f x y = true and f y z = true implies f x z = true
type comparison.t 'a = a -> a -> bool.t;

The type-checker isn't going to enforce whatever semantics you assign to a type, so making a new type for every contract is just boilerplate.

3
  • 1
    I don't think your first point addresses the question - it's a function definition, not a signature. Of course by looking at the implementation of a class or a function you can tell what it does - the question asks can you still preserve the semantics at a level of abstraction (an interface or function signature)
    – Alex
    Jun 17, 2015 at 10:45
  • @AlexFoxGill - it's not a function definition, although it does uniquely determine foldr. Hint: the definition of foldr has two equations (why?) while the specification given above has three. Jun 17, 2015 at 14:10
  • What I mean is that foldr is a function not a function signature. The laws or definition are not a part of the signature
    – Alex
    Jun 17, 2015 at 14:18
-1

Pretty much all statically-typed functional languages have a way to alias basic types in a way that requires you to explicitly declare your semantic intent. Some of the other answers have given examples. In practice, experienced functional programmers need very good reasons to use those wrapper types, because they hurt composability and reusability.

For example, say a client wanted an implementation of a message query that was backed by a list. In Haskell, the implementation could be as simple as this:

messages = ["Message 0", "Message 1", "Message 2"]
messageQuery = (messages !!)

Using newtype Message = Message String this would be much less straightforward, leaving this implementation looking something like:

messages = map Message ["Message 0", "Message 1", "Message 2"]
messageQuery (Id index) = messages !! index

That might not seem like a big deal, but you either have to do that type conversion back and forth everywhere, or set up a boundary layer in your code where everything above is Int -> String then you convert it to Id -> Message to pass to the layer below. Say I wanted to add internationalization, or format it in all caps, or add logging context, or whatever. Those operations are all dead simple to compose with an Int -> String, and annoying with an Id -> Message. It's not that there are never cases where the increased type restrictions are desirable, but the annoyance had better be worth the trade off.

You can use a type synonym instead of a wrapper (in Haskell type instead of newtype), which is much more common and doesn't require the conversions all over the place, but it doesn't provide the static type guarantees either like your OOP version, just a bit of enapsulation. Type wrappers are mostly used where the client isn't expected to manipulate the value at all, just store it and pass it back. For example, a file handle.

Nothing can prevent a client from being "subversive." You're just creating hoops to jump through, for every single client. A simple mock for a common unit test often requires weird behavior that doesn't make sense in production. Your interfaces should be written so they don't care, if at all possible.

5
  • 1
    This feels more like a comment on Ben/Daenyth's answers - that you can use semantic types but you don't because of the inconvenience? I didn't downvote you but it's not an answer to this question.
    – Alex
    Jun 17, 2015 at 10:48
  • -1 It would not hurt composability or reusability at all. If Id and Message are simple wrappers for Int and String, it is trivial to convert between them. Jun 17, 2015 at 12:43
  • Yes, it's trivial, but still annoying to do all over the place. I've added an example and a little bit describing the difference between usage of type synonyms and wrappers. Jun 17, 2015 at 13:11
  • This seems like a size thing. In small projects, these kinds of wrapper types probably aren't that useful anyway. In large projects, it's natural that the boundaries will be a small proportion of the overall code, so doing these types of conversions at the boundary and then passing around wrapped types everywhere else isn't too arduous. Also, like Alex said, I don't see how this is an answer to the question. Jun 18, 2015 at 9:58
  • I explained how to do it, then why functional programmers wouldn't do it. That's an answer, even if it's not the one people wanted. It has nothing to do with size. This idea that it's somehow a good thing to intentionally make it more difficult to reuse your code is decidedly an OOP concept. Creating a product type that adds some value? All the time. Creating a type exactly like a widely-used type except you can only use it in one library? Practically unheard of, except perhaps in FP code that interacts heavily with OOP code, or written by someone mostly familiar with OOP idioms. Jun 18, 2015 at 13:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.