DEV Community

Taqmuraz
Taqmuraz

Posted on

OOP via FP : functional nature of classes and objects

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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(),
]
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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(),
]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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(),
]
Enter fullscreen mode Exit fullscreen mode

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(),
]
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
fst2000 profile image
Sergey

I would like to see examples where the functional approach shows itself better than the OOP

Collapse
 
taqmuraz profile image
Taqmuraz

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.

Collapse
 
efpage profile image
Eckehard

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.

Thread Thread
 
taqmuraz profile image
Taqmuraz • Edited

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 :

nums = range(0, 10000)
loop(nums, some_function)
Enter fullscreen mode Exit fullscreen mode

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 :

[
  10.times.each println, # writing 10 numbers as an output
  println (10.times.pipe 0, +) # writing sum of elements in range from 0 to 10
]
Enter fullscreen mode Exit fullscreen mode

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.

Thread Thread
 
efpage profile image
Eckehard

About finding out, where and in which context your function will be called -- if it has no side-effects, then why does that matter?

can you always avoid using "this"?

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.

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.

Thread Thread
 
taqmuraz profile image
Taqmuraz

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.

Collapse
 
taqmuraz profile image
Taqmuraz

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.

Collapse
 
efpage profile image
Eckehard

There are different rules for motorways and for normal streets. Is this a reason to never drive on a motorway?

Collapse
 
taqmuraz profile image
Taqmuraz

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 :)

Collapse
 
efpage profile image
Eckehard

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.

Thread Thread
 
taqmuraz profile image
Taqmuraz • Edited

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 :

using System;
using System.Collections.Generic;
using System.Linq;

interface ILogger
{
    void Debug(string message);
    void Info(string message);
    void Error(string message);
}
class ConsoleLogger : ILogger
{
    void Print(ConsoleColor color, string level, string message)
    {
        Console.ForegroundColor = color;
        Console.WriteLine($"[{level}] {message}");
    }
    public void Debug(string message) => Print(ConsoleColor.Gray, "Debug", message);
    public void Info(string message) => Print(ConsoleColor.Green, "Info", message);
    public void Error(string message) => Print(ConsoleColor.Red, "Error", message);
}
class WrapLogger : ILogger
{
    List<ILogger> loggers;
    internal WrapLogger(params ILogger[] loggers) => this.loggers = loggers.ToList();
    public void Debug(string message) => loggers.ForEach(l => l.Debug(message));
    public void Info(string message) => loggers.ForEach(l => l.Info(message));
    public void Error(string message) => loggers.ForEach(l => l.Error(message));
}
Enter fullscreen mode Exit fullscreen mode

And that

using System;
using System.Linq;

delegate void Printer(string message);
delegate (Printer debug, Printer info, Printer error) Logger();

class Logging
{
    internal static Logger ConsoleLogger() {
        Printer MakePrinter(ConsoleColor color, string level) => message => {
            Console.ForegroundColor =  color;
            Console.WriteLine($"[{level}]{message}");
        };
        return () => (
            MakePrinter(ConsoleColor.Gray, "Debug"),
            MakePrinter(ConsoleColor.Green, "Info"),
            MakePrinter(ConsoleColor.Red, "Error")
        );
    }
    internal static Logger WrapLogger(params Logger[] loggers)
    {
        var list = loggers.Select(l => l()).ToList();
        return () => (
            message => list.ForEach(l => l.debug(message)),
            message => list.ForEach(l => l.info(message)),
            message => list.ForEach(l => l.error(message))
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Of course, it may be about preferences and opinions, my interest is to share my own ones.

Thread Thread
 
taqmuraz profile image
Taqmuraz

In addition to my last comment, Jena code would look this way

consoleLogger = () -> {
  .debug:message -> print "[Debug]" message line,
  .info:message -> print "[Info]" message line,
  .error:message -> print "[Error]" message line,
} =>
wrapLogger = loggers ->
  key -> message ->
    loggers.map key.each logger -> logger message
=> ()
Enter fullscreen mode Exit fullscreen mode
Collapse
 
evgenijshelukhin profile image
evgenijShelukhin

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.

Collapse
 
taqmuraz profile image
Taqmuraz

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 :)