DEV Community

OOP via FP : functional nature of classes and objects

Taqmuraz on February 07, 2024

Topic for today Today I would like to tell you, why such OOP concepts as classes and objects are just particular cases of functions, usi...
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 :)