ООП без ООП

ООП без ООП

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# класс все равно создать придеться).

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, но это кажется совсем не существенно. 

Продолжение следует...


Report Page