(λ) Function Oriented Programming (FOP)

(λ) Function Oriented Programming (FOP)

A functional procedural alternative to OOP, developed for multi-paradigm languages
 
notion image
 
(λ) FOP – a methodology based on Procedural Programming (PP) using Functional Programming (FP) techniques, developed for multi-paradigm languages.
 
To work with (λ) FOP in your language, you must be able to write 2 things:
 
Data Schema (Data) – ideally using typing, but class notation is also possible.
// Ideally it should be typing type User = { id: string email: string password: string } // ... but class notation will also work class User { constructor( public id, public email, public password, ) {} }
 
Function – the good old construct that can (1) accept data structures as input, (2) return a result, (3) be passed as an argument.
 
// Function / procedure / lambda, depending on the language function changeUserEmail( user: User, newEmail: string, sendEmailNotification: (email: string, body: string) => void ) { if (user.email === newEmail) { throw new Error(`Email must be different`) } user.email = newEmail sendEmailNotification(user.email, `New email assigned`) }
 
 
If you try to visualize a (λ) FOP application, you'll get chains of functions transforming data:
 
notion image
💡
So the main principle of FOP is data and functions exist separately from each other and any function can use any data.

Table of Contents

Instead of a gigantic long read (in pdf format), I decided to hide the contents of the chapters under expandable lists, and put the longest ones on separate pages.
 
You just need to sequentially move from section to section ("Versus", "Origins of FOP", etc.), expand the chapters and read them.
 
And on all separate pages, I placed "previous/next chapter" buttons at the bottom.
 
Enjoy reading.

1. Versus

If you have a purely OOP language – write in OOP. If you have a purely FP language – write in FP.
 
FOP occupies the niche of multi-paradigm languages, in which you have a choice between different methodologies, such as Javascript, TypeScript, Rust, Go, Python, Kotlin, etc.
 
🥊 FOP vs OOP
  • You've never written in "pure OOP" anyway Few backend developers have written in canonical OOP (except for Java, C# and people using DDD). It's easy to check: How many of your classes just hold dependencies and operate on models? They're probably called …Service or …Manager. If many, then this is no longer canonical OOP, but its mixture with procedural/functional programming. The main danger of this mixture is that you'll lose the main advantage of OOP (all operations on an "object" are within it) and at the same time incur all the problems of OOP. FOP, in turn, very clearly describes its boundaries and rules, so you won't face such a problem.
  • Canonical OOP doesn't allow programs to develop quickly When using OOP, if you incorrectly divided the data or designed connections, you'll have to do a lot of refactoring or create workarounds, like dozens of …Service/…Manager classes. The problem isn't that you could make this mistake when creating the system, no, the main problem is that your system will continue to evolve and along with that will come new requirements that don't fit into the current graph of "objects". In FOP, you won't face such a problem: each separate function operates on the set of data it needs, and when new requirements appear, you simply fix the logic in the necessary functions or write new ones.
  • Forget about Inheritance, Class Polymorphism and Abstraction, as well as principles like SOLID, KISS, DRY and all 23 design patterns. This picture was created as a joke, the problem turned out to be that it's true:
    • notion image
      All these patterns are needed to solve OOP problems that it created itself... And if there's no OOP, then there are no such problems. OOP is so abstract and complex that you have to first solve the complexity of OOP itself, before you can start solving the complexity of the program:
      OOP: "Keep It Simple Stupid" Also OOP: "Remember 4 pillars, 1 is already in programming, 2 are anti-patterns, and the last one is so abstract that everyone interprets it as they want. You also need to learn KISS, DRY and SOLID, where everyone forgets the meaning of the 3rd letter, and you shouldn't ignore 4-5, so wrap every bit of code with interfaces. There are also design patterns, 36 of them, learn and constantly remember the difference between virtual and abstract constructors, although you can sometimes forget to leave a little room for the diamond problem and cross cutting concern."
 
🥋 FOP vs FP
  • Low entry thresholdFOP does not require monads, functors, semigroups and setoids at all, while currying, immutability and pure functions are optional.
  • Presence of FP featuresAt the same time, FOP actively uses things such as ADT, functional composition, currying, polymorphism on interfaces, declarative style and other techniques that allow writing beautiful and efficient code.
  • FamiliarOne of the most frequent phrases I've heard when working with FOP: "Wow, it turns out I was practically writing my code like this!" – so any developer who has written in OOP or FP will find FOP extremely understandable.
  • Available in any multi-paradigm languageLanguages that were not developed for the FP model, simply won't give the advantages of FP. FOP, on the other hand, is minimalist and works in any language that has functions and data structure description.
🧑‍🦽 FOP vs PP
More details in chapter
💔
2.4. Why FP and PP are not the way out
, but in short:
 
  • Best-practice Procedural programming is a rather outdated paradigm, which also lacks any modern techniques for writing flexible and convenient code. FOP uses modern techniques such as ADT, Branded Types, composition, currying, and optionally pure functions.
  • Declarative style Procedural programming is completely based on the imperative style. This is not bad, and in FOP we will often use imperative approaches, but the declarative approach gives more opportunity to write flexible and human-understandable code.
More details in chapter 2.4. Why FP and PP are not the way out

2. Origins of (λ) FOP

Problems that led to the emergence of FOP:
 
🧐
2.1. You Don't Know What OOP Is
🥲
2.2. You've Never Used OOP
☠️
2.3. The Unfixable Problem of OOP
💔
2.4. Why FP and PP are not the way out
🎉
2.5. That's why (λ) FOP emerged

3. Why (λ) FOP works

In previous section I’ve described why OOP is NOT working, and here I’ll tell why FOP does:
 
🤸
3.1. Flexibility - the most important property of code
⛓️
3.2. Process First Design
💾
3.3. Data Oriented Architecture
👇
3.4. What you need, where you need it (WYNWYN): Context Dependence and Code Connectedness
🎭
3.5. Explicit is better than implicit
🪚
3.6. Occam's Chainsaw or "Throw away everything, absolutely everything"

4. Pillars

For better understanding, let's compare the pillars of FOP with the pillars of OOP:
 
🌗 Separation of Data and Behavior
Behavior (functions) and Data (objects, types, primitives, etc.) in OOP are combined with each other within the framework of Encapsulation:
 
// In OOP, Data and Behavior live within classes // /user.ts class User { // Data private _id: string private _email: string private _password: string // Behavior setNewPassord(newPassword: string) { const newEncryptedPassword = encrypt(newPassword) if (this._password === newEncryptedPassword) { throw new Error(`New password must not be the same`) } this._password = newEncryptedPassword } }
 
In FOP, the opposite principle is used, where Data and Behavior (functions) exist separately from each other, and any function can use any data:
 
// In FOP, Data and Behavior live separately from each other // (even from the file system point of view) // ./user.ts // Data type User = { id: string email: string password: string } // ./set-user-new-password.ts // Behavior const setUserNewPassword = (user: User, newPassword: string) { const newEncryptedPassword = encrypt(newPassword) if (this._password === newEncryptedPassword) { throw new Error(`New password must not be the same`) } this._password = newEncryptedPassword }
 
The consequences of this are:
 
  1. All Data properties should be public (no private / protected)
  1. "Natural encapsulation" is achieved
  1. "Context dependence" is achieved
  1. The Cross Cutting Concern problem does not exist
  1. True DRY is achieved
 
🍔 Composition over inheritance
Inheritance from OOP involves creating generalized entities and detailing them through "inheritance" by more specialized entities.
 
If we visualize Inheritance, we get a Tree, where the root is the highest parent class:
 
notion image
 
// Parent class class Animal { constructor( private position: number = 0, ) {} walk() { this.position += 1; } } // Child classes class Dog extends Animal { constructor( private voice: string ) {} bark() { console.log(this.voice) } } class Cat extends Animal { constructor( private jumpHeight: number ) {} jump(obstacleHeight: number) { if (this.jumpHeight > obstacleHeight) { console.log(`Success`) } else { console.log(`Oooops`) } } }
 
But Composition involves creating specialized entities from which more generalized ones are assembled.
 
So we get an inverted Tree:
 
notion image
 
And this applies to both Data and Behavior:
 
// DATA COMPOSITION // From smaller Data ... type Walker = { position: number } // ... we assemble larger Data ... type Dog = { walker: Walker voice: string } type Cat = { walker: Walker jumpHeight: number } // ... and write behavior for them const walk = (walker: Walker) => { walker.position += 1 } const bark = (dog: Dog) => { console.log(dog.voice) } const jump = (cat: Cat, obstacleHeight: number) => { if (cat.jumpHeight > obstacleHeight) { console.log(`Success`) } else { console.log(`Oooops`) } } // BEHAVIOR COMPOSITION // Number functions const addOne = (value: number) => value + 1 const multiplyByTwo = (value: number) => value * 2 // And here's a composition creating a new one from existing functions const addOneAndMultiplyByTwo = (value: number) => multiplyByTwo(addOne(value))
 
The main advantage of Composition is the possibility of absolute flexibility in extending data and behavior, while reducing the complexity of each individual component.
 
For an example of the power of Composition and the disadvantages of Inheritance:
 
// We need to create 3 dogs: // 1. A living Dog that walks, barks and farts // 2. A dead dog that only farts // 3. A robot dog that walks and barks // Implementation in Composition style type Walker = { position: number } const walk = (walker: Walker) => { walker.position += 1 } type Farter = { noise: string } const fart = (farter: Farter) => { console.log(farter.noise) } type Barker = { voice: string } const bark = (barker: Barker) => { console.log(barker.voice) } type Dog = { walker: Walker farter: Farter barker: Barker } type DeadDog = { farter: Farter } type RobotDog = { walker: Walker barker: Barker } // . Now try to do this using Inheritance and you'll be surprised // at how complex your implementation will be. // ...
 
🧬 Polymorphism on interfaces
Polymorphism is a mechanism that allows us to structure logic for its further reuse.
OOP uses "Polymorphism on inheritance":
 
// Class describing the presence of testicles and the ability to cut them off class BallsOwner { _balls: boolean cutBallsOff() { this._balls = false console.log("😢") } } // Classes owning testicles (and their additional properties) class Cat extends BallsOwner { _clawsLength: number } class Dog extends BallsOwner { _teethLength: number } // A class that will use the parent class, using polymorphism class Villain { constructor( name: string, ) {} castrate(ballsOwner: BallsOwner) => { ballsOwner.cutBallsOff() } } const cat = new Cat() const dog = new Dog() const villain = new Villain("Valera") villain.castrate(cat) villain.castrate(dog)
 
In FOP, many variants of "Polymorphism on interfaces" are used:
 
// 1. POLYMORPHISM ON COMPOSITION type BallsOwner = { balls: boolean } type Cat = { ballsOwner: BallsOwner clawsLength: number } type Dog = { ballsOwner: BallsOwner teethLength: number } const cat: Cat = { ballsOwner: { balls: true }, clawsLength: 10 } const dog: Dog = { ballsOwner: { balls: true }, clawsLength: 10 } const castrate = (ballsOwner: BallsOwner) => { ballsOwner.balls = false console.log("😢") } castrate(cat.ballsOwner) castrate(dog.ballsOwner) // 2. POLYMORPHISM ON UNION TYPE type Cat = { clawsLength: number balls: boolean } type Dog = { teethLength: number balls: boolean } type Fish = { finLength: number balls: boolean } // This type uses Union Type (|) type BallsOwner = Cat | Dog | Fish const castrate = (ballsOwner: BallsOwner) => { ballsOwner.balls = false console.log("😢") } // 3. POLYMORPHISM ON DISCRIMINANT UNION type Cat = { type: "Cat" balls: boolean } type Dog = { type: "Dog" balls: boolean } type Fish = { type: "Fish" balls: boolean } // This type uses Discriminated Union (|) type BallsOwner = Cat | Dog | Fish const castrate = (ballsOwner: BallsOwner) => { ballsOwner.balls = false // . Check by type switch ballsOwner.type { case "Cat": console.log("MEOW 😢") case "Dog": console.log("BARK 😢") case "Fish": console.log("... 😢") } } // 4. BEHAVIOR POLYMORPHISM type Cat = { clawsLength: number balls: boolean } type Dog = { teethLength: number balls: boolean } type Fish = { finLength: number balls: boolean } // . Implementation of the function phrase about losing balls const catBallsLoosingPhrase = (cat: Cat) => if (!cat.balls) "MEOW 😢" const dogBallsLoosingPhrase = (dog: Dog) => if (!dog.balls) "BARK 😢" const fishBallsLoosingPhrase = (fish: Fish) => if (!fish.balls) "... 😢" // This type uses Union Type (|) type BallsOwner = Cat | Dog | Fish // Type describing behavior polymorphism type BallsLoosingPhrase = (ballsOwner: BallsOwner) => string const castrate = (ballsOwner: BallsOwner, phrase: BallsLoosingPhrase) => { console.log(phrase(ballsOwner)) ballsOwner.balls = false }
 
🥚 Natural Encapsulation
"Object" encapsulation is created due to the closure of data and behavior in "objects":
 
notion image
 
Such barriers to data access and behavior between each other lead to:
 
  1. All "objects" begin to refer to and use each other's methods, turning debugging into a nightmare
  1. When new entities appear, we have to come up with and embed new "objects" in this graph of connections
  1. When refactoring 1 feature, a huge number of "objects" can be affected because it passes through them
  1. Most class methods will exist to be called once by one other class.
 
This is also called: "putting a lock on the right pocket for the left hand" – why do this when the left hand is your own hand?
 
An OOP developer might object and say: "All this simply means that you made a mistake in the design and it's time to change the structure of objects, using dozens of patterns created specifically for such cases"
 
But in fact, if OOP had not been used in the first place, these problems would simply not have arisen, and therefore no refactoring would be required.
 
Natural encapsulation is the hiding of code that happens by itself.
 
For example, natural encapsulation exists at the level of any service: all the code base of your application is hidden within it and it only gives out some API to the outside.
 
notion image
 
Another good example would be library code – any library gives out only specific methods and entities that you will use, leaving a bunch of implementations in its depths.
 
You can think of a whole service or library as one big "object", where all properties (data) and methods (functions) are available to each other.
 
Using natural encapsulation is very simple – describe the data and let functions use all this data. If you want to add boundaries to the code, start breaking it into libraries/services/modules/domain areas/whatever.
 
  1. Independent functions can be arranged in an acyclic graph, so there will be no deadloops.
  1. When new entities appear, it is enough to simply describe the data structures and write functions that work with them.
  1. According to FOP, one feature will most likely be in 1 function that uses multiple data, so for refactoring, you can touch just 1 function.
  1. If something needs to be done only once in 1 function, it will be written directly in that function, because it has the right to work with any data (not just those in its "object" properties)
 
Thus, by abandoning OOP, we solve at the root the problems created by OOP itself.

5. Useful Techniques

Optional, but very convenient techniques for writing functionally procedural code:
 
Branded types / Custom primitives
Typing itself is validation of the type at compilation time and forces us to transform and ensure the correctness of the type:
 
const someFn = (val: string) => { //... } const someOtherFn = (val: string | number | null) => { // ... // We'll have to do a check first to make sure of the data type // otherwise the program won't compile if (typeof val === number || val === null) { throw new Error(`Not correct type`) } someFn(val) }
 
So why not develop this idea further and extend our types:
 
// Our custom type type Email = string // Our<b> </b>function checking that the string is an Email const emailFromString = (val: string): Email => { if (!val.inludes("@") { throw new Error(`Not correct email`) } return val as Email } // Example of another function accepting our branded type const someFn = (val: Email) => { //... } const val: string = "some@mail.com" someFn( <u>val</u> // There will be an error here during compilation/in IDE // because string cannot be used instead of Email ) someFn( emailFromString(val) // And here everything is fine )
 
This way you can create a huge number of different types: UUID, UserId, StringMin50Symbols, etc.
 
The only problem is whether your language can correctly interpret that the custom type Email is not actually a string?
 
For example, in the example above, you could simply pass a string to someFn and it would accept it.
 
To solve this problem, the Branded types technique is used, which involves turning the type into a conditionally unique one (that is, unique only at the compilation level):
 
// In Go it's very simple type Email = string // In TypeScript type Email = string & { readonly "Email": unique symbol }
 
Now the code above will work correctly.
 
We will consider the ways to implement Branded type for each individual language in the chapter below "💬 Programming Languages".
 
Pattern Matching
The task is this: "we have parsers for two types of devices, we need to make a parsing function that will use one or another type of parser".
 
How such a problem is solved in OOP:
 
// # Create implementations of the first and second parser class FirstTerminalParser { parse: (data: string): void { // ... } } class SecondTerminalParser { parse: (data: string): void { // ... } } // # Describe a common interface type Parser = { parse: (data: string) => void } // # Implement our function const someFunction = (data: string, parser: Parser) => { parser.parse(data) }
 
We used a technique called "Interface polymorphism": since our classes fit the interface of the argument, we can use them in this place.
 
This is one of many solutions available in OOP, but like most of them (especially the most common ones), it uses "abstraction", in the form of an interface.
 
And here's how the same problem is solved in the Functional style:
 
// # We can also use class notation if we want class FirstTerminalParser { parse: (data: string): void { // ... } } class SecondTerminalParser { parse: (data: string): void { // ... } } // # This time our interface doesn't "generalize", but specifically indicates "either/or" through Union Type type Parser = FirstTerminalParser | SecondTerminalParser // # Implement our function const someFunction = (data: string, parser: Parser) => { parser.parse(data) }
 
As you can see, the difference is just 1 line, but in fact the depth of this difference is incredible:
 
  1. First, "Parser" is now not something abstract, but specifically one of two types (which means all type hints will work even better)
  1. Second, we can now use Pattern Matching
 
class FirstTerminalParser { name: "FirstTerminalParser"; // # Add a field by which we'll do Pattern Matching parse: (data: string): void { // ... }; firstTerminalSpecificFunction: (): string { // ... }; } class SecondTerminalParser { name: "SecondTerminalParser"; // # Same key, but different value parse: (data: string): void { // ... }; secondTerminalSpecificFunction: (): number { // ... }; } type Parser = FirstTerminalParser | SecondTerminalParser; const someFunction = (data: string, parser: Parser) => { switch (parser.name) { case "FirstTerminalParser": return parser.firstTerminalSpecificFunction(); case "SecondTerminalParser": return parser.secondTerminalSpecificFunction(); // Switch guard or "check for finite number of options" default: const arg: never = parser; // # This is a so-called Switch Safe Guard throw new Error(`Undefined ${parser}`) } };
 
Which allows us to use specific functions of a particular implementation when needed.
 
And another HUGE advantage is that thanks to the check for finite number of options (the construct in default), if we add a new type to Parser and forget to add a case for its processing, the error will pop up at the moment of compilation.
 
At first it may not be very obvious, but my big advice: try instead of abstraction, where you have a choice between several options, to use Union Type and Pattern Matching and then you can understand the full power of this approach.
 
ADT and Invariants
Looking at this structure, can you determine which variations would be valid for business logic:
 
type User = { id: string email: string | undefined isEmailActivated: boolean }
 
You might assume that isEmailActivated cannot be true if email: undefined, BUT THIS IS JUST AN ASSUMPTION.
 
What if we do it like this:
 
type User = { id: string email: undefined isEmailActivated: false } | { id: string email: string isEmailActivated: false } | { id: string email: string isEmailActivated: true }
 
Now we clearly see which variations are appropriate for our application logic.
 
Invariants are variations of data values that are correct for our application logic.
 
Let's make this more convenient and human-readable:
 
type UserEmailEmpty = { email: undefined isEmailActivated: false } type UserEmailUnactivated = { email: string isEmailActivated: false } type UserEmailActivated = { email: string isEmailActivated: true } type UserEmail = UserEmailEmpty | UserEmailUnactivated | UserEmailActivated type User = { id: string userEmail: UserEmail }
 
Now, we clearly see all the invariants of user email states.
 
Algebraic Data Type (ADT) is the description of each individual invariant (like UserEmailEmpty, UserEmailUnactivated, UserEmailActivated), and their combinations (UserEmail).
 
Just for knowledge expansion: the description of an individual variant in ADT is called a "product type," and their combinations are called a "sum type". You can read more in Wikipedia if you understand what's written there.
 
Also, ADT gives us amazing possibilities for pattern matching.
 
But the implementation of ADT differs greatly from language to language, so we'll look at ADT techniques in more detail in the "💬 Programming Languages" section.
 
! IMPORTANT CLARIFICATION ! The more complex your invariants are, the more reliable your program is, but they become much harder to refactor. Therefore, my advice: write complex invariants only when the logic of a certain area has already been sufficiently established and is not going to change frequently.
 
Declarative Programming
Imperative programming describes "how to do something":
 
To get information about a user with id = 1, you need to open a file located in a specific folder, then read the contents, parse it, ..., assemble the user structure, and return it.
 
In declarative programming, you describe "what needs to be done":
 
Get me the user with id = 1
 
The most obvious example of a declarative language is SQL.
 
You don't tell SQL: "to get information about a user with id = 1, you need to open a file, ..." - you say: "Get me the user with id = 1", or in other words: SELECT * FROM USER WHERE id = 1 - and then the database engine performs all the necessary imperative actions underneath.
 
SQL is clear, but how do you write "declarative code" in JS or a similar language?
 
I thought a lot about how to simplify the answer and realized that the simplest description is:
 
In a declarative style, you should have a dedicated function for each operation
 
Example with try-catch:
 
// Imperative try catch let result: string try { result = await someFn("Hello world!") } catch (e) { console.log(e) } // Declarative try catch const tryCatch = async( tryFn: () => any, catchFn: (e: any) => any ): Promise<R> => { try { await tryFn() } catch (e) { await catchFn(e) throw e } } let result await tryCatch( () => result = someFn("Hello world!"), (e) => console.log(e) )
 
It might seem like: "What is this nonsense? Why do we need this?" - here's your answer:
 
// 1. Declarative try catch with return (<R> is a generic for result type) const tryCatchReturn = async <R>( tryFn: () => R, catchFn: (e: any) => any ): Promise<R> => { try { return await tryFn() } catch (e) { await catchFn(e) throw e } } // This can be a very convenient alternative instead of writing // let result outside try catch every time const result = await tryCatchReturn( () => someFn("Hello world!"), (e) => console.log(e) ) // 2. Or we can return either the result or an error const tryCatchEither = async <R>( tryFn: () => R ): Promise<R | Error> => { try { return await tryFn() } catch (e) { if (e instanceof Error) return e throw e } } // This is useful when we want to be sure that // we've handled the error const result = await tryCatchEither(() => someFn("Hello world!")) if (result instanceof Error) { // ... } // 3. And we can conveniently create a tryCatch function // that will always log the error and throw it further const tryCatchLog = (logger: Logger) => { return (tryFn: () => any) => { return tryCatchReturn( tryFn, (e) => { logger.error(e) } ) } } // Now somewhere at the beginning of the program, add a logger to it const logger = new Logger() const tcl = tryCatchLog(logger) // And now we can use it like this const result = tcl(() => someFn("Hello world"))
 
Here's another example with validators:
 
// How we can imperatively check login validity const login = "HelloZalupa" const imperativeCheckLogin = (login: string) => { if (login === "" || login.length < 6 || login.includes("Zalupa")) { throw new Error(`Incorrect login`) } } // Now let's do it declaratively const isNotEmptyString = (val: string): string => { if (val === "") throw new Error(`String must not be empty`) return val } const stringMin6Symbols = (val: string): string => { if (val.length < 6) throw new Error(`String must be at least 6 symbols`) return val } const stringHasNoBadWord = (val: string): string => { if (val.includes("Zalupa")) throw new Error(`String must not include zalupa`) return val } const declarativeCheckLogin = (val: string) => { return stringHasNoBadWord( stringMin6Symbols( isNotEmptyString( val ) ) ) }
 
There can be hundreds of such functions and their compositions.
 
The key is that each function performs some small operation, and you can compose more complex functions from them.
 
And this is the main advantage of the declarative approach – maximum flexibility and reusability.
 
Therefore, even if your language is not inherently declarative (like SQL), you can still write declarative code, it will just be a bunch of functions, each containing a bit of imperative code.
 
P.S.
You definitely need to use declarativeness carefully, because you can create such a small pile of functions that they will be inconvenient to work with. Therefore, in FOP we prefer the declarative approach, but we can absolutely write imperatively without any problems.
 
Immutability and Pure Functions
Pure functions are functions that satisfy 2 properties: (1) given the same input, they produce the same output, (2) they don't create side effects (changing external state or accessing external systems).
 
Now let's have a test:
 
// Which functions are pure and which are not const fn1 = (a: string): string => { return a + ", hi!" } const fn2 = (a: { name: string }) => { a.name += ", hi!" return a } const fn3 = (a: { name: string }) => { fn2(a) return a.name + ", hi!" } const fn4 = (a: { name: string }) => { return { name: a.name + ", hi!" } } const fn5 = (a: { name: string }) => { const newName = fn4(a) newName.name += " Bye!" reutrn newName } let counter = 0 const fn6 = () => { counter += 1 return `Updated ${counter} times` } const fn7 = () => { let counter = 0 return () => { counter += 1 return `Updated ${counter} times` } } const fn8 = (a: number): number => { console.log(Math.random()) return a * 2 } const fn9 = ( db: {update: (byId: string, email: string) => void}, id: string, newEmail: srtring ) => { return db.update(id, newEmail) } const fn10 = (db: {getById: (id: string) => User}, id: string) => { return db.getById(id) }
 
And here are the answers:
 
// Pure const fn1 = (a: string): string => { return a + ", hi!" } // Not pure, because it changes external state (argument) const fn2 = (a: { name: string }): { name: string } => { a.name += ", h1!" return a } // Not pure, because it uses another impure function (fn2) const fn3 = (a: { name: string }) => { fn2(a) return a.name + ", hi!" } // Pure, because it doesn't change anything const fn4 = (a: { name: string }) => { return { name: a.name + ", hi!" } } // Not pure, because it changes the return value of an internal pure function (fn4) const fn5 = (a: { name: string }) => { const newName = fn4(a) newName.name += " Bye!" reutrn newName } // Not pure, because it changes external state (external variable) let counter = 0 const fn6 = () => { counter += 1 return `Updated ${counter} times` } // This can be called pure const fn7 = () => { let counter = 0 // But this is an impure function because it changes external state (external variable) return () => { counter += 1 return `Updated ${counter} times` } } // Not pure, because it creates 2 side effects: // 1. console.log - this changes external state (stdout.write) // 2. Math.random() - this is a side effect because it accesses the OS to get a seed for randomness const fn8 = (a: number): number => { console.log(Math.random()) return a * 2 } // Not pure, because it creates 2 side effects: // 1. It accesses an external system (db) // 2. It changes state (db.update) const fn9 = ( db: {update: (byId: string, email: string) => void}, id: string, newEmail: srtring ) => { return db.update(id, newEmail) } // Not pure, because it creates a side effect: it accesses an external system (db) const fn10 = (db: {getById: (id: string) => User}, id: string) => { return db.getById(id) }
 
Two questions arise:
 
  1. What if we need to access external systems (db)?
  1. What if we still need to change something?
 
For the first question, the answer is: we separate "impure" functions that will work with external systems from "pure" functions that will transform this data.
 
// Types type User = { id: string email: string // password: string - this field exists in the DB, but we don't want to retrieve it into the program } type DB = { updateById: (id: string, user: User) => void getById: (id: string) => User } // Impure function for working with the database const updateUser = ( db: db, user: User, ) => { return db.updateById(user.id, user) } // Impure function for working with the database const getUserById = (db: DB, id: string) => { const res = db.getById(id) return { id: res.id, email: res.email } } // Pure function for business logic const assignNewUserEmail = (user: User, newEmail: string) => { if (!newEmail.includes("@") { throw new Error(`Email is invalid`) } if (user.email === newEmail) { throw new Error(`New email must be different`) } return { ...user, email: newEmail, } } // Now we combine them const updateUserEmailController = (db: DB, userId: string, newEmail: string) => { const user = getUserById(db, userId) const updatedUser = assignNewUserEmail(user, newEmail) updateUser(db, updatedUser) }
 
In fact, we end up with a layered cake: IO → Business Logic → IO
 
And you can have as many layers as you want.
 
What if we still need to change something?
 
Then we need to create a copy of this argument, modify the copy, and return it:
 
type User = { firstName: string; secondName: string; fio: string | undefined; } const addFIO = (user: User): User => { return { ...user, // Copy all properties fio: user.firstName + user.secondName // add a new field } }
 
This is called Immutability – instead of changing the entity itself, we create a copy of it and modify the copy.
 
Important life hack: in most languages, working with immutable structures is not optimized, so it's perfectly normal if you mutate entities that you created in the same function:
 
const someFn = (someArray: string[]) => { const array = [] for (let i = 0; i < someArray.length; i++) { // We can modify array because it was created in this same function array.push(someArray[i] + ", hi!") } return array }
 
Advantages of immutability and pure functions:
 
Predictability
Often underestimated, but an extremely useful property of code.
 
Here's an example:
 
const predictabilityExample = async (db: DB) => { const user = await db.getUser() const userSubscriptions = await db.getUserSubscriptions(user.id) const userWallet = await db.getUserWallet(user.id) await changeUserSubscriptionPlan(user, userSubscriptions, userWallet) }
 
What exactly from user, userSubscriptions, and userWallet will change during the execution of changeUserSubscriptionPlan?
 
This is a question we can only answer by looking at changeUserSubscriptionPlan itself.
 
And if there are 1000 lines of code, calls to other functions and libraries? Then it will be difficult for us.
 
How will immutability help us?
 
Like this:
 
const predictabilityExample = async (db: DB) => { const user = await db.getUser() const userSubscriptions = await db.getUserSubscriptions(user.id) const userWallet = await db.getUserWallet(user.id) const [updatedUserSubscriptions, updatedUserWallet] = await changeUserSubscriptionPlan(user, userSubscriptions, userWallet) }
 
Now we know for sure that changes will happen to the userSubscriptions and userWallet entities, and user itself will not be changed during the execution of this code.
 
This consequence can be described as follows:
 
If a function returns a structure that was passed to it in arguments, then somewhere inside there was a transformation of this structure
 
The advantage of this consequence is difficult to reveal in a small example, but the more code you write, the more transformations it produces, the harder it is to keep track that you did not perform a transformation where you did not want to and vice versa.
 
And if you also need to check what exactly has changed, we can use the following advantage of immutability:
 
Delta comparison
The mechanism on which React and Redux are built.
 
Their essence is as follows: if you need to check "has a change occurred", then using immutability makes this very simple, because if even one field changes, lying deep deep in our reference variable, then the reference itself also changes and you can simply compare by reference:
 
type User = { id: string username: string team: { id: string name: string members: User[] } } const shallowComparisonExample = async (db: DB) => { const user: User = await db.getUser() const updatedUser = await someFunctionThatUpdateSomethingInUser(user) if (user !== updatedUser) { // It doesn't matter what changed and how deeply in user, if // any change occurred, user and updatedUser will have // different references } }
 
In addition, we can easily check exactly what has changed in the same way:
 
type User = { id: string username: string team: { id: string name: string members: User[] } } // Very simplified function for checking changes const compare = (recordA: Record<any, any>, recordB: Record<any, any>) => { // Iterate through Record properties for (const property in recordB) { if (recordA[property] !== recordB[property]) { console.log(`Property ${property} changed from ${recordA[property]} to ${recordB[property]}`) // Simplified check if the field is also an object if (typeof recordB[property] === 'object') { compare(recordA[property], recordB[property]) } } } } const shallowComparisonExample = async (db: DB) => { const user: User = await db.getUser() const updatedUser = someFunctionThatUpdateSomethingInUser(user) if (user !== updatedUser) { compare(user, updatedUser) } }
 
React and Redux use this property of immutability.
 
For example, on the backend, I had to implement a simplified Unit of Work pattern:
 
const UserDataService = (db: DB) => { let users = {} return { getUserById: (id: string): User => { const user = await db.getUser({id: id}) users = { ...users, [user.id]: user, } }, saveUser: (updatedUser: User): Promise<User> => { // The getChangedPropertiesAndValues function returns an object only with changed // fields and their values. It works very quickly because I know that my code // is immutable, which means that if the user has changed at all, it's enough to make a simple check const delta = getChangedPropertiesAndValues(updatedUser, users[updatedUser.id]) if (delta) { await db.saveUser(delta) } return updatedUser } } } const uowExample = async (db: DB, userId: string) => { const userDS = UserDataService(db) const user: User = await userDS.getUserById(userId) const updatedUser = someFunctionThatUpdateSomethingInUser(user) await userDS.saveUser(updatedUser) }
 
Race-conditions
They seemingly don't exist in languages without parallelism, but that's not true. Here's an example in JS:
 
const raceConditionCounter = async () => { let counter = { value: 1 } await Promise.all([ async () => counter.value = counter.value - 1, async () => counter.value = counter.value * 4, async () => { if (counter.value === 0) throw new Error() counter.value = counter.value / 2 }, ]) console.log(counter.value) }
 
What value will be displayed in the console?
 
The correct answer: always different, because we don't know the order in which these promises will execute.
 
Therefore, if you need to write logic that will run a set of processes on an entity "in parallel", to be sure that the result of each of them will not affect the other, we can use immutability:
 
const raceConditionCounter = async () => { let counter = { value: 1 } const [decrementedCounter, mulipliedCounter, dividedCounter] = await Promise.all([ async () => { value: counter.value - 1 }, async () => { value: counter.value * 4 }, async () => { if (counter.value === 0) throw new Error() return { value: counter.value / 2 }, }, ]) console.log(counter.value, decrementedCounter.value, mulipliedCounter.value, dividedCounter.value) // 1, 0, 4, 0.5 }
 
From personal experience, I've encountered such situations when working with parsers and ETL processes.
 
Version History
Imagine you need to have a "change history" and the ability to roll back to a previous state.
 
If each change creates a new object, it means we can store each version of the object in an array. And therefore, at the right moment, roll back to its previous states.
 
Yes indeed, Ctrl+Z often works using immutability.
 
Ease of Testing
Testing pure functions is a pleasure.
 
Nothing extra happens in them, you don't need to initialize anything in advance, you just describe different variations of input arguments and check them against what you want to see in the output.
 
Disadvantages of immutability and pure functions:
 
Code Design
First, if you're going to mutate entities and at the same time don't know how / your language doesn't allow you to write pipes, then you'll end up with the following:
 
const user = getUser() const userUpdatedEmail = updateUserEmail(user) const userUpdatedEmailAndAge = updateUserAge(userUpdatedEmail) const userUpdatedEmailAgeAddedRewards = addRewards(userUpdatedEmailAndAge) const userToSave = updateUserAge(userUpdatedEmailAgeAddedRewards) saveUser(userToSave)
 
It's very easy to get confused in such a situation. This looks absolutely horrible.
 
Pipes can improve the situation:
 
// Soon such an operator will appear in JS, // but it exists in functional languages like Elixir const user = getUser(userId) |-> updateUserEmail |-> updateUserAge |-> addRewards |-> updateUserAge |-> saveUser // An alternative could be composition const user = saveUser( updateUserAge( addRewards( updateUserAge( updateUserEmail( getUser(userId) ) ) ) ) ) // Or using third-party libraries, BUT I'll say right away, // in TS for example, such functions are very difficult to type // if they use generics const user = pipe( getUser, updateUserEmail, updateUserAge, addRewards, updateUserAge, saveUser, )(userId)
 
The example is not from real code, but even it still looks terrible.
 
What's worse is that rarely can we conveniently divide functions into small pieces, so you'll have to put in real effort to write such code.
 
Working with Layered IO
You need to very clearly divide which functions will work with IO, and which will work with business logic.
 
Sounds simple, but try to rewrite some piece of code in a similar manner and you'll understand what the real problem is, and it's quite serious.
 
Performance
However it may be, if your language doesn't have built-in immutable structures, then immutability will always be slower than mutability.
 
Here's an article that describes how immutable structures work.
 
Yes, you can, of course, use special libraries, but the speed increase will still not exceed the speed of mutability.
 
For many programs and programmers, the disadvantages outweigh the advantages, so in FOP, you can use immutability and pure functions at your discretion.
 
The recommendation would still be to use them, but that's a matter of taste and preference.
Closureful & Closureless Functions
If a function uses closure variables, it is called closureful, and if not, then closureless:
 
// counter is a closure for the statefulFn function let counter = 0; const closurfulFn = () => { counter = counter + 1; } // in this situation, the function works only with input arguments, // so it doesn't use closure const closurelessFn = (counter: number) => { return counter + 1; }
 
Throughout the codebase, we will have to decide which approach to use, especially when we want to write some "module" / "library" or something similar.
 
Here's an example of a closureful Vector2 module:
 
// 1. Closureful behavior, enclosing state // 1.1. In class notation class Vector2 { constructor( public x: number, public y: number, ) {} add(vec: Vector2): Vector2 { return new Vector2(this.x + vec.x, this.y + vec.y) } } const firstVector = new Vector2(1,2) const secondVector = new Vector2(3,4) const summedVector = firstVector.add(secondVector) // 1.2. Or in functional notation type Vector2 = { x: number y: number add: (vec: Vector2) => Vector2 } const newVector2 = (x: number, y: number) => { return { x, y, add: (vec: Vector2): Vector2 => { return Vector2.new(x + vec.x, y + vec.y) } } } const firstVector = newVector2(1,2) const secondVector = newVector2(3,4) const summedVector = firstVector.add(secondVector)
 
And here's Closureless:
 
// 2. Closureless behavior // 2.1. In class notation class Vector2 { constructor( public x: number, public y: number, ) {} // Static methods allow avoiding closures static add(firstVec: Vector2, secondVec: Vector2): Vector2 { return new Vector2(firstVec.x + secondVec.x, firstVec.y + secondVec.y) } } const firstVector = new Vector2(1,2) const secondVector = new Vector2(3,4) const summedVector = Vector2.add(firstVector, secondVector) // 1.2. Or in functional notation type Vector2 = { x: number y: number } const newVector2 = (x: number, y: number): Vector2 => { return {x, y} } const add = (firstVec: Vector2, secondVec: Vector2): Vector2 => { return newVector2(firstVec.x + secondVec.x, firstVec.y + secondVec.y) } const firstVector = newVector2(1,2) const secondVector = newVector2(3,4) const summedVector = add(firstVector, secondVector)
 
To understand when to choose Closureless and when Closureful, it's enough to use 2 rules:
 
  1. If you're creating a library for other developers, use the approach that is common in your language. For example, for JS, Python, PHP, this will be the Closureful approach.
  1. In all other cases, always use Closureless functions.
 
This follows from applying the principle
👇
3.4. What you need, where you need it (WYNWYN): Context Dependence and Code Connectedness
: since library functions should be used in multiple places (contexts), they can exist next to the data (closureful), but in other cases, Closureless should be preferred.
 
The following technique illustrates this principle very well: "Utils & App Specific Data"
 
Utils & App Specific Data
In any program, there are 2 types of data:
 
  1. Utils (Utility) – data that could be used in absolutely any program:
      • Email – because Email has a correct and understandable form
      • Vector – because it's a scientific concept, with a described form and operations that we can perform on it
      • GOST132 – this is a form of profit calculation that is documented at the government level
      • PostgreSQL Driver – code that allows us to communicate with our PSQL database
      • Logger – code that allows us to create logs
      • and so on
       
  1. App Specific – data and behavior that are unique to our application/system:
      • User – although in 70% of cases this data is similar between applications, it will be unique for yours
      • Order – the form and actions that we will perform on some "order" differ greatly from application to application
      • and so on
 
And exactly where you will place functions that work with App Specific Data will determine in which style your code is written: OOP, FOP, FP, etc.
 
In FOP, the principle is as follows:
 
  • Functions for Utils Data can be written right next to them The thing is, in 90% of cases, the Behavior of Utils Data is quite well understood and won't expand too much, so we can describe it completely in one place:
    • // File ./vector.ts // We can feel free to fully describe the Behavior of Vector2 right in // the file with the Vector2 description // In Stateful style type Vector2 = { x: number y: number add: (vec: Vector2) => Vector2 } const newVector2 = (x: number, y: number): Vector2 => { return { x, y, add: (vec: Vector2): Vector2 => { return Vector2.new(x + vec.x, y + vec.y) } } } // ... or in Stateless type Vector2 = { x: number y: number } const add = (firstVec: Vector2, secondVec: Vector2): Vector2 => { return Vector2.new(firstVec.x + secondVec.x, firstVec.y + secondVec.y) }
       
  • Functions for App Specific Data should NOT be located next to them
    • The logic of Behavior for App Specific Data depends veeeery heavily on the context of a specific function, so we don't even try to "generalize" it and put it closer to this data, but instead write this logic where we specifically need it (even constructor functions):
       
      // File ./database-schema.ts type User = { id: string email: string password: string } // Somewhere in ./register-user.ts const registerUser = (email: string, password: string) => { // ... const user: User = { id: uuid.v4(), email, password, } // ... } // Somewhere in ./change-user-email.ts const changeUserEmail = (user: User, newEmail: string) => { // ... user.email = newEmail; // ... }
       
      It is through this approach that "Context Dependence" is maintained and code coupling is reduced.
 
By following these rules, you will get all the benefits of the function-oriented approach.

6. Programming Languages

How to use FOP with real code examples in:
 
6.1. What a language needs for FOP
The set of requirements for languages is extremely small and fits all popular multi-paradigm programming languages.
 
Required
 
To work with FOP in your language, you only need 2 entities:
 
Data structure description – description of structures that our functions will work with:
 
// This can be a type / interface type User = { id: string email: string password: string } // Or even a class, if your language doesn't support typing class User { constructor( public id: string public email: string public password: string ) {} }
 
Function – the good old construct that can (1) take data structures as input, (2) return a result, (3) be passed as an argument.
 
// Function / procedure / lambda, depending on the language function changeUserEmail( user: User, newEmail: string, sendEmailNotification: (email: string, body: string) => void ) { if (user.email === newEmail) { throw new Error(`Email must be different`) } user.email = newEmail sendEmailNotification(user.email, `New email assigned`) }
 
 
It's important to understand that FOP, like OOP, is not syntax (this idea is elaborated in the "Origins of FOP" section).
 
Therefore, both paradigms can be written with or without class syntax (class).
 
This depends on personal preferences and language features, but the general recommendation is NOT to use class syntax in FOP.

Nice to have

 
Optional, but very convenient
 
  1. Modules. The ability to group functions for convenient export and use in code.
  1. Branded types. The ability to validate specific data types during compilation.
  1. ADT – the ability to describe invariants through types.
 

Optional

This functionality will help you do some tricks, but you can program in FOP without it:
 
  1. Type inheritance. The ability to inherit types from each other.
  1. Immutability and Pure functions. This term speaks for itself.
  1. Stateful behavior. The ability to close over state and methods for working with it.
 
All these constructs are definitely available in TypeScript, so I recommend reading about it to understand how they work.
 
(coming soon) 6.2. TypeScript
(coming soon) 6.3. Rust
(coming soon) 6.4. Golang
(coming soon) 6.5. Python
(coming soon) 6.6. Kotlin
 
Looking for contributors who are ready to help port FOP to these languages, as I simply don't have enough time to do it. All interested parties, please message me on Telegram @davidshekunts

7. Useful Links

Here you will find implementations that will help you apply practices from FOP in your projects:
 
  • λ FapFop.ts – library of FOP patterns for TypeScript

8. About the Author

notion image
Hello! My name is David Shekunts and I'm a Full-Stack Tech Lead working with Go & TS
 
Throughout my career, I felt that OOP didn't quite click with me; it seemed "excessive." FP is wonderful, but still too marginal and requires languages tailored for it, which makes it very difficult to build a team.
 
Only when I encountered Go and its simplest and very expressive procedural approach, I finally found what was closest to the convenient and effective coding style I was looking for.
 
BUT already having experience in FP, I didn't want to lose features like separation of data and behavior, disjoint unions, pattern matching, currying, immutability safety, and so on.
 
Therefore, at the intersection of Procedural and Functional programming, FOP was born as a methodology that allows me and my colleagues to write understandable and flexible code based on just a few simple rules and concepts described above.
 
I and all the teams I've worked with or trained use FOP every day. Among them:
 
🏭 GoMining
🎰 Smartvend
🔞 Astkol
💳 Nearpay
🧠 Deeplay
🏎️ ZenCar
 
(if your team also uses FOP, write to me, I'll add you to the list)
 
So far, FOP has been performing exceptionally well: there hasn't been a task that we couldn't solve using FOP quickly and simply (both for developing new features and refactoring existing ones).
 
The transition from OOP, PP, and FP to FOP is very straightforward because it incorporates the simplest and most understandable mechanics familiar to developers from each style.
 
I hope your journey will be just as pleasant.
 
Wishing everyone powerful growth 💪