Topic for today
Today I would like to tell you, why such OOP concepts as classes and objects are just particular cases of functions, using examples of code in Ruby and in my own language, Jena.
Why Ruby? Despite that it is an object-oriented language, it also provides a good foundation for functional approaches. In that, I hope, my article will convince you, though it is a secondary point today.
Jena references
If you are not familiar with Jena, you may read dedicated article about this language.
If you want to try Jena, you are welcome to visit its github repository.
Object as a table of functions
Let us watch on an example of a class declaration in Ruby :
class Human
def walk
puts "walking!"
end
def stand
puts "standing!"
end
end
h = Human.new()
h.walk
h.stand
Class is a popular idea, that has wide spread in modern languages, such as Python, Java, C#, C++.
What exactly do we see here?
Class Human
seem to be a function, that creates objects.
But, also, object h
here has two functions, and we may call them by their names.
May we try to implement the same behaviour, using only functions? Let us try :
def Human
return {
:walk => -> do puts "walking!" end,
:stand => -> do puts "standing!" end,
}
end
h = Human()
h[:walk].call
h[:stand].call
I feel important to explain, what I am doing here. Function Human
creates a hash-map, where symbols are associated with
anonymous functions. Yes, :walk
and :stand
are symbols, it is a part of Ruby syntax, very uncommon in other languages.
As you may see, I chose Ruby for a reason. This language has one thing in common with Jena -- symbol literals.
Honesty and clarity
More honest implementation of an object through a function would be this one :
def Human
map = {
:walk => -> do puts "walking!" end,
:stand => -> do puts "standing!" end,
}
return ->key do map[key] end
end
h = Human()
h.call(:walk).call
h.call(:stand).call
Now we are not using a dedicated syntax, hiding object implementation behind a function. You may say, that Human
class code does look better, and code with functions is verbose and complicated. It is true, because syntax of Ruby (and most of other object-oriented languages) is designed to make use of classes and objects easy, sacrificing for that ease and clarity of functions.
Let me demonstrate the same behaviour, implemented in Jena :
Human = () -> {
.walk:() -> println "walking!",
.stand:() -> println "standing!",
} =>
h = Human() => [
h.walk(),
h.stand(),
]
This code does exactly the same thing that the last Ruby code does : creates a Human
function, that returns table of functions, calls it, storing result as h
and, after that, calls walk
and stand
functions, taking them from table, using .walk
and .stand
symbols as keys.
Mutable state
You may say, that objects have a mutable state, and, sometimes, it may be very useful. Methods may change attributes of an object, but may functions do the same?
Short answer is yes. Let us write some Ruby code
class Human
@x
def initialize
@x = 0
end
def moveLeft
@x -= 1
end
def moveRight
@x += 1
end
def status
puts "x = #{@x}"
end
end
h = Human.new()
h.moveLeft()
h.moveRight()
h.moveRight()
h.status()
How would look the same code in our approach, where object is a function?
def Human
x = 0
map = {
:walkLeft => -> do x -= 1 end,
:walkRight => -> do x += 1 end,
:status => -> do puts "x = #{x}" end,
}
-> key do map[key] end
end
h = Human()
h.call(:walkLeft).call
h.call(:walkRight).call
h.call(:walkRight).call
h.call(:status).call
And, in Jena it would be implemented next way :
Human = () -> x = box(0) => {
.walkLeft:() -> x.apply(-1),
.walkRight:() -> x.apply 1,
.status:() -> print ("x = " + (x.get)),
} =>
h = Human() => [
h.walkLeft(),
h.walkRight(),
h.walkRight(),
h.status(),
]
Dynamic languages, such as Ruby, provide us flexibility in control of program state, we don't have to explicitly allocate memory for a mutable state, local variables already do present a state.
Performance
Our function-based objects have one serious flaw : low performance. We are building a functions table each time we create a new object, can we optimize that? Yes, of course we can, all we need is to separate an object state from functions table :
$table = {
:walkLeft => -> obj do obj[:x] -= 1 end,
:walkRight => -> obj do obj[:x] += 1 end,
:status => -> obj do puts "x = #{obj[:x]}" end,
}
def Human
state = { :x => 0 }
-> key do
-> do
$table[key].call(state)
end
end
end
h = Human()
h.call(:walkLeft).call
h.call(:walkRight).call
h.call(:walkRight).call
h.call(:status).call
And Jena :
Human = table = {
.walkLeft: obj -> obj.x.apply(-1),
.walkRight: obj -> obj.x.apply 1,
.status: obj -> print ("x = " + (obj.x.get)),
}
=> state = { .x:(box 0) }
=> () -> key -> () -> table key state =>
h = Human() => [
h.walkLeft(),
h.walkRight(),
h.walkRight(),
h.status(),
]
Looks overloaded? I agree. But, we may simplify that. Let us create a class factory function, so we may distract from building table and state functions each time we want to create a class :
Class = table ->
constructor ->
args -> state = constructor args =>
key -> arg -> table key args state arg =>
Human = Class {
.walkLeft: () -> obj -> () -> obj.x.apply(-1),
.walkRight: () -> obj -> () -> obj.x.apply 1,
.status: () -> obj -> () -> println("x = " + (obj.x.get)),
} args -> { .x:(box (args.x)) } =>
h = Human{.x:5} => [
h.walkLeft(),
h.walkRight(),
h.walkRight(),
h.status(),
]
As you see, now we even can pass constructor arguments to build our object, it is a full complete object-oriented class, implemented by functions only.
Other reasons to choose functions over objects
For many of readers solution with classes looks more familiar and pleasant, but only because you already have experience using that solution.
Functions are coming from a mathematical world, their abstraction and distraction from implementation details are attractive indeed. Functions have simple and consistent concept behind, while objects are very specific and domain-oriented.
Finally, class in most of the object-oriented languages is a separate syntax, what brings more complexity and involves a special logic for constructors, methods and attributes.
That's all for today. Thank you for reading, and please, share your thoughts in comments.
Top comments (14)
I would like to see examples where the functional approach shows itself better than the OOP
Thanks for this question. Here are common advantages of functional approaches :
Purity and clarity (program does not depend on side-effects)
Data safety : data is immutable, you don't have to validate it more than once.
Easy to reuse : functions have no mutable state, that makes them safe to use and easy to test and debug.
Program interface simplicity : functional approaches are declarative, that means -- program does describe what it is going to do, but not how.
Function composition and brevity : you may create a function by composing two other functions, while in object oriented programming we have to declare a new class each time we want to compose objects of other classes.
Multithreading : purity of functions makes multithreading seem natural for a program, while in object oriented programming we have to fight with data races.
Though I studied the concepts of FP a lot, some of the "advantages" are hardly noticeable for me. Often it is hard to see, what a function will do, if you pass functions as arguments. It can be a nightmare to find out, in which context a function is running. At least in Javascript this is not clarity, but a shell game.
Referring to "immutability", this is more a theoretical idea than a real advantage. How to you write an "immutable" loop? You need to use recursion, which simply means to make use of the stack insead of a simple counter. This is slow and a waste of memory. A loop from 1 to 1000 will create 1000 new contexts for your function, a loop from 1 to 1Mio will probably end up with a stack overflow.
Recursions are hard to debug, as all loops run on the same code. But instead of a loop variable, your counters are stored one by one somewhere in the stack.
From a theoretical point of view, I can see the advantges, but from a practical view I prefer the simple and straightforward solution.
I may agree with some of your points.
About finding out, where and in which context your function will be called -- if it has no side-effects, then why does that matter?
About "immutable" loop -- I think, idea is not about fact of immutability, but about abstraction from mutability. You may create a function
loop
, that accepts some collection to iterate and some function to process each pass. Something like that :And here you have abstraction from a loop function implementation. You don't need it to be implemented with recursion.
In Jena you have two ways to iterate through collections :
.each
and.pipe
:Both these methods have native Java implementation and use
for
loop to iterate, but from Jena code you would never know that.Also, some functional languages, such as Clojure, have recursion optimizations, what allows to use it with no risk of stack overflow.
We don't have to accept functional approaches literally, avoiding any mutations and procedural actions. We have to localize them, isolate them from majority of code, make our program independent from idea of mutable state.
can you always avoid using "this"?
So, we can use objects in functional code too, right? Object are isolated by design, as the code is defined as an abstraction (-> template) in the class.
In JavaScript I definitely would avoid use of "this".
In other languages (Java, Python, Ruby) I would use it as a recursive reference to self as a function.
Yes, of course we can use objects.
Object with one method we may consider as a function, no doubts.
Object with two or more methods would be a functions hash-map (name -> function).
But, in most cases I would prefer explicit functions or hash-maps of functions over objects, because objects and classes are useful only for special cases, not always.
By the way -- in this article we have functions with mutable state. It is a very uncommon thing in functional programming, when function has a state.
It was just necessary to show that in case of necessity we may have a mutable state. Because, in some domain areas (game programming, as example), it is easier to implement some model by a mutable state, rather than a pure function.
There are different rules for motorways and for normal streets. Is this a reason to never drive on a motorway?
Looking from this perspective, I would say that there is no reason to constantly avoid object oriented design. We are free to choose approaches we like.
If you see object-oriented approaches from C#, Java or Python as most comfortable, then enjoy them :)
OO-design is far from comfortable (like driving very fast), but it is a most helpful design pattern. It can help to organize your code and build reusable units. But it takes much more effort and and requires more thought than writing spaghetti code. Decisions made in one of the core classes often prove to be good or bad much later, so writing OO-code often requires a lot of refractorinig.
Once you have a class - or should we say - a whole family ready, you are sure you have a well insulated part oft your code that could run in other environments too.
This is - in short words - the strength of OOP.
But what happens inside a class? This is mostly simple procedural or functional code. There is no good reason not to follow the rules of functional programming if you build your code.
Class as a particular tool helps in special cases.
But, most of the time it feels like additional pair of wheels for a bicycle.
Compare that :
And that
Of course, it may be about preferences and opinions, my interest is to share my own ones.
In addition to my last comment, Jena code would look this way
I've some positive experience with the Typescipt. What do you think about the Typescript? It also supports both functional style and oop style and me personally did not have troubles neither reading nor debugging the code.
It is a good question.
I had no experience with TypeScript, but know a little about this language.
Strict typing I see as a good trait, that makes code more readable and easy to understand. But, at the same time, it significantly increases language complexity, that's why I still prefer the dynamic typing.
With strict typing I find self thinking more about solution, not about task itself :)