# 01 - OOP and Functional Programming

## OOP - Object-Oriented Programming

OOP is the programming paradigm that revolves around using **classes** or otherwise custom objects to organise a codebase, by having all data regarding a certain entity, along with functions to operate on or use that data, grouped within the same class/object.

OOP is often used because it can lead to cleaner codebases, and because it permits using abstraction, inheritance, encapsulation or polymorphism.

## FP - Functional Programming

FP is the programming language paradigm that revolves around using **functions** to operate on data passed as arguments, rather than grouping data into a class/object.

FP is more rarely used because it's more limiting than OOP, but it also provides more readability at first sight, since it eliminates hidden behaviour of dunder methods.

## Examples

An example calculator program could be:

### For FP

```python
def calculate(expression: str) -> int:
    items = expression.split(" ")
    a, b = int(items[0]), int(items[2])
    operator = items[1]

    match operator:
        case '+':
            return a + b
        case '-':
            return a - b
        case '*':
            return a * b
        case '/':
            return a // b

x = calculate('2 + 3') # 5
x = calculate(f'{x} + 2') # 7
print(x) # 7
```

Notice how there is no persistent state from the function? That's the core of FP.

### For OOP

```python
from typing import Self

class Calculator:
    def __init__(self, value: int = 0) -> None:
        self.value = value

    def calculate(self, expression: str) -> Self:
        expression = expression.replace("x", str(self.value))
        items = expression.split(" ")
        a, b = int(items[0]), int(items[2])
        operator = items[1]
        
        match operator:
            case '+':
                self.value = a + b
            case '-':
                self.value = a - b
            case '*':
                self.value = a * b
            case '/':
                self.value = a // b

        return self

    def __str__(self):
        return str(self.value)

calc = Calculator()
calc.calculate('2 + 3')
calc.calculate('x + 2')
print(calc)  # 7

# Or:
calc = Calculator(2)
calc.calculate('x + 3').calculate('x + 2')
print(calc)
```

Notice how, with OOP, the class itself implements persistence rather than requiring the user to implement it themselves via variable assignments? That gets more useful when you need multiple values.

## Summary

Functional Programming lets you understand what everything does quicker at first sight, at the cost of requiring you to store data in variables and pass it through parameters.

Object-Oriented Programming lets you organise data and functions within one class/object, reducing the burden of implementation from the user, at the cost of hidden behaviour that might not be expected at first sight (such as `x` being replaced with the stored value, in the example above).
