3

In FP vs. OO, from the trenches, Michael Fogus says:

  • Whenever I write some code to deal with data about people then functional programming seems to work best.

  • Whenever I write some code to simulate people then object-oriented programming seems to work best.

My question is: In a pure FP language, how do you "simulate objects"? Some languages let's you define a Module, but what are the other options? Are there any specific design patterns to address this specific

  • 2
    Possible duplicate of How are objects modelled in a functional programming language? – gnat Jul 28 '16 at 9:54
  • 1
    @gnat That's very useful indeed. – 53777A Jul 28 '16 at 9:58
  • 3
    I suspect that mostly the FP view will be that there is no difference between modeling data about people and simulating people – jk. Jul 28 '16 at 10:09
  • 3
    @gnat: unfortunately, the answers in that question seem to be exclusively about modeling mutable state, which is completely orthogonal to modeling objects. – Jörg W Mittag Jul 28 '16 at 15:07
  • 2
    @JörgWMittag agree, thanks for catching! I retracted dupe vote and edited title of the other question to avoid mistakes like that in future – gnat Jul 28 '16 at 17:36
8

Objects are about functional abstraction and dynamic dispatch. Functional abstraction is obviously trivial in functional languages. Dynamic dispatch can be implemented using first-class functions, again, those should be available in a functional language.

Basically, an object is a dispatch function which returns functions (methods) which are nested in a common closure (shared state).

function makeNewObject(sharedState) {
  const add = function (amount) { return makeNewObject(sharedState + amount);};
  const sub = function (amount) { return makeNewObject(sharedState - amount);};

  const toString = function () { return "I am an object with value: " + sharedState; }

  return function (method) {
    if (method === 'add')      return add;
    if (method === 'sub')      return sub;
    if (method === 'toString') return toString;
  }
}

const iAmAnObject = makeNewObject(42);
const iAmAlsoAnObject = iAmAnObject("add")(23);

console.log(iAmAlsoAnObject("toString")())
// I am an object with value: 65

You may note that this is essentially how objects are implemented in ECMAScript, except for the fact that the if cascade is replaced with a hashtable (confusingly called "object" in ECMAScript) and there is some syntactic sugar around constructor functions with the new keyword. That's not terribly surprising, since ECMAScript is heavily based on Scheme, and the above is basically how you implement objects in λ-calculus and Scheme.

  • @8bittree: I could wax poetically about the difference between messages and the methods implementing those messages and that this nicely demonstrates that principle, but … um … yeah, it was a typo. Nice catch, thanks! – Jörg W Mittag Jul 28 '16 at 15:21
  • 1
    There was, admittedly, some temptation to just ignore it, since it still would have, technically, worked, but I felt it may have been a bit of a distraction if it remained. Of course, if a question about the difference between messages and methods comes up, I now fully expect you to provide a poetic answer. – 8bittree Jul 28 '16 at 15:27
3

@JorgWMittag's answer gives a very good theoretical grounding on a very important theoretical concept: an object can be simulated using a closure that selects behaviour based on a parameter. Unfortunately, this approach has a few problems:

  • You need to write code to interpret the individual messages received by an object, which is something that you'd usually want your language to do for you
  • In statically typed languages you'll have problems with different methods that handle different argument types.

One possible solution for the above two problems is to encode your protocol using an algebraic data type. For example, in Haskell, Jorg's code might look something like this:

data MyObjectProtocol = MyObjectProtocol_Add Int |
                        MyObjectProtocol_Sub Int | 
                        MyObjectProtocol_ToString
type MyObject = MyObjectProtocol -> Either MyObject String

makeNewObject :: Int -> MyObject
makeNewObject state = handleRequests
    where
       handleRequests (MyObjectProtocol_Add amount) = Left $ makeNewObject $ state + amount
       handleRequests (MyObjectProtocol_Sub amount) = Left $ makeNewObject $ state - amount
       handleRequests (MyObjectProtocol_ToString) = Right $ "I am an object with value: " ++ show state

This still leaves a couple of problems:

  • The various return types of the functions must be encoded in another type, which you'll have to pattern match on when you use the object, which is a long way from convenient.
  • The way I've done this can't handle a method that both mutates the object and returns a value in an easy way. Probably the best way of solving this is to turn the object into a monad, but that makes the implementation quite complicated.

It would be better, therefore, to use multiple functions to encode the methods of the object and implement something a little more like virtual dispatch as used by most OO languages today, i.e. by storing a table of virtual method pointers (aka functions) in the object. You can then have a set of standard functions that fetch the virtual method from the object and execute it. This might look something like this:

data MyObject = MyObject (Int -> MyObject) 
                         (Int -> MyObject)
                         (String)
myObject_add :: MyObject -> Int -> MyObject
myObject_add (MyObject f _ _) amount = f amount
myObject_sub :: MyObject -> Int -> MyObject
myObject_sub (MyObject _ f _) amount = f amount
myObject_toString :: MyObject -> String
myObject_toString (MyObject _ _ f) = f

makeNewObject :: Int -> MyObject
makeNewObject state = MyObject add sub toString where
    add amount = makeNewObject $ state + amount
    sub amount = makeNewObject $ state - amount
    toString = "I am an object with value: " ++ show state

The result is a little more boilerplate in the definition of the object type, but makes it much easier to use the object. And if you have different implementations of the object type, the amount of boilerplate for each additional implementation is lower, so this is definitely a better way of working for interfaces that might have many implementations.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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