ООП без ООП
gavrЯ часто сталкивался с тем что в функциональных языках необходимо держать в памяти множество функций. Конечно часто помогает то что они разделены по модулям, например List.map() или List.filter()
```Gleam
import gleam/list
[1, 2, 3, 4]
|> list.filter(fn(x) { x % 2 == 0 })
|> list.map(fn(x) { x * 2 })
|> io.debug
```
```Clojure
(->>
'(1 2 3 4)
(map inc) ;=> (map inc '(1 2 3 4))
(filter odd?) ;=> (filter odd? (map inc '(1 2 3 4))
(into [])) ;=> (into [] (filter odd? (map inc '(1 2 3 4)))
```
```Rescript
let list = [1, 2, 3, 4];
List.map(
value => value + 1,
List.filter(
value => value % 2 == 0,
list
)
)
```
Но это бесполезно с более специфичными типами данных. Чаще всего я только примерно представляю какие операции я хочу произвести и единственное решение это идти и искать в документации что-то похожее в документации.
### OOP
ООП в каком то смысле решило эту проблему поместив функции внутрь типов. Теперь я могу сделать `object. ` и в выпадающем окне увидеть все возможные действия с этим объектом, впервые такая функциональность появилась разумеется в Smalltalk, как и само понятие IDE Топ статья часть 1 часть 2
С++ алгол-образное ООП в свою очередь стало мейнстримом и привнесло множество новых проблем, которые замечательно обозначены в видео Object-Oriented Programming is Bad https://youtu.be/QM1iUe6IofM
### UFCS
В языке Nim эту проблему решили по другому, они реализовали UFCS - universal function call syntax. Иными словами в процедурном языке вызов add(1, 2) можно записать как 1.add(2), первый аргумент каждой функции также выступает в качестве получателя, тем самым добавляя возможность автодополнения не вводя при этом ООП.

Эта идея мне очень нравится, пока не вводят UFCS вместе с обычным ООП как в DLang. Теперь вы не можете сказать читая код foo.bar() это метод класса Foo или обычная функция принимающая Foo первым аргументом.
### Extension methods
Также существуют методы расширения. В ООП языках они решают проблему добавления нового функционала к third-party классу(например из библиотеки которую нельзя изменять)
Простой пример, мы можем расширить встроенные тип String собственным методом wordCount вместо того чтобы создавать какой то странный StringUtils класс(правда в C# класс все равно создать придеться).

Под капотом это работает точно также как UFCS, просто создается функция принимающая первым аргументом

### Niva
Niva это типизированный Smalltalk, то есть все является объектом, у всего есть методы (в отличии от функционального подхода), также не существует top-level функций, из чего следует что мне больше всего подойдет подход extension methods.
Однако мне также нравится подход UFCS из nim (niva = nim + vala) так что почему бы все не упросить и не сделать все методы методами расширения.
То есть снаружи кажется что все как обычно, однако на самом деле все является статическими функциями принимающими первым аргументом получателя(то что стане `this`). А значит будет работать куда производительнее
Статический вызов - самый дешевый и легко-оптимизируемый
- Отсутствует привязка к объекту
- Не нужно собираться в поход в v-table, нет полиморфизма - не нужно определять какой метод нужно вызывать в зависимости от типа объекта(спойлер теперь это делаем руками)
- Для статических методов компилятор может применять самые агресивные виды оптимизаций особо не задумываясь(прогрев JIT) ведь про них все известно
String wordCount = this split: "" "Hello Extension Methods" wordCount
Вот виды вызовов в порядке замедления
- INVOKESTATIC
- INVOKEVIRTUAL - извлекаем ссылку на метод из пула констант, проверка что объект является объектом указанного класса или его подкласса, поиск метода у фактического класса (dynamic dispatching)
- INVOKEINTERFACE - извлечение ссылки на интерфейс из пула констант, проверка реализует ли объект этот интерфейс, поиск конкретного метода по иерархии классов, вызов
- INVOKEDYNAMIC - неизвестно ничего, вызов скорее всего был через рефлексию, аля каждый вызов js/python
Конечно после нескольких походов без изменений, когда резолвится будет одно и тоже, жит это запомнит и станет снова дешево.
----
В niva нет наследования, интерфейсов, абстрактных классов, @Override и всего остального присущего ООП, но все же основным способом вычислений остается посылка сообщений, это Smalltalk!
### Где тогда взять полиморфизм
Специально приведу этот дебильный пример c вики:

Так вот моим ответом являются tagged unions - суммы типов с тагом на каждый из вариантов.

Я назвал методы makeSound woof и meow по разному исключительно для ясности, они вполне могли бы все называться одинаково так как у них разные типы получателя.
Каковы бенефиты?
Как я упоминал ранее теперь вместо v-table мы вручную выбираем метод в зависимости от тега(метод `Animal makeSound`). Что это дает? Если предположить что с JIT производительность INVOKEINTERFACE станет такой такой же как статик, у нас все еще остается явность.
Перейдя к определению makeSound мы явно увидим все возможные варианты и можем легко перейти к конкретным реализациям. Я множество раз сталкивался в продакшоне с нагромождениями огромного количества уровней абстракций(Clean Code like), что собственно является частой критикой OOP.
Второе преимущество это exhaustive. Если мы добавим нового наследника Animal в предыдущем варианте все match сразу же загорятся красным потребовав реализовать недостающий вариант.
Также все это просто занимает меньше места и легче читается.
### Но все это уже есть в моей Java/C\#/Dart
Да, похожий концепт был добавлен в Java 17 и C#. Но
1) Огромное количество кода написано и продолжает писаться не используя sealed
2) Миграция на новые версии происходит очень медленно
3) Там все еще есть все остальное
4) Не везде присутствует exhaustive check (например его нет в C#)
В niva же нет другого способа достичь полиморфизма, что и является самым основным отличием, это эксперимент.
### Проблема расширяемости
> Это все хорошо, но ведь главное преемущество наследования в том что мы можем добавить новых наследников к классам из стороних библиотек, а с tagged union это невозможно
Это верно и здесь у меня есть 2 решения, 1 теоретических и 1 практическое
Позволить добавлять новые ветки в tagged union других пакетов невозможно, так как пропадает гарантия exhaustive check, что будет если в функцию сторонней библиотеки попадет наш новый вариант для которого нет обработчика?
1) Добавить второй механизм полиморфизма работающий статически
1) Структурная типизация(TS, Go) Если библиотеки нужен объект с полем name типа String, мы можем подсунуть туда любой подходящий
2) Шаблоны с мономорфизацией - пишем дженерик функцию делая с типом T что угодно, компилятор запоминает все действия с объектом генерируя ограничения на его тип. При вызове функции компилятор проверяет на соответствие полученного объекта этих ограничений и генерирует обычную функцию где вместо T уже подставлен тип этого объекта, то есть моно-морфизируя ее.
2) В тупую передавать в функцию сторонней библиотеки данные в том виде в котором она их принимает, трансформируя свои. Это подход в лоб и он хорошо своей простотой, ценой небольшой потери производительности из-за доп нагрузки на GC, но это кажется совсем не существенно.
Продолжение следует...