LINUX.ORG.RU

ООП. Как представляете себе идеальную реализацию?

 , ,


2

3

Я знаю как минимум 4 слабо совместимых друг с другом понятия ООП:

  • С++: класс = неймспейc, вызов метода через точку,
  • CLOS: класс = идентификатор + наследование, тело метода определяется по классу всех параметров (а не только первого), методы доопределяются модификаторами :after :before :around.
  • Racket: класс = first-class object, как и функция, соответственно, может доопределяться по месту и не иметь имени.
  • Haskell: классы типов как наборы операций над типам (которые можно считать эквивалентными классам других языков)

Хотелось бы узнать, какой подход вам ближе и почему. А также, можно ли из упомянутых видов получить некую «идеальную реализацию» и какой синтаксис мог бы быть у такой реализации.

★★★★★

Ответ на: комментарий от no-such-file

то просто получишь ответ «no-such-method»

Так и в крестах ты получишь тот же ответ. Только на этапе компиляции - и это хорошо.

Выше уже согласились, что работает только в жёсткой статике:

class Object {};
Object *obj = deserialize(file);

try {
    obj->new_run();
} except {
    obj->run();
}

не компилируется

class Object {};

class ObjectV1: public Object 
{ public: virtual void run(); };

class ObjectV2: public ObjectV1 
{ public: virtual void new_run(); };

Object *obj = deserialize(file);

try {
    (ObjectV2 *)obj->new_run();
} except {
    (ObjectV1 *)obj->run();
}

всегда выполняет new_run, даже если deserialize вернул new ObjectV1().

monk ★★★★★
() автор топика
Последнее исправление: monk (всего исправлений: 2)
Ответ на: комментарий от monk

Именно в таком случае помогает dynamic_cast, но он проверяет не то и вместо простой проверки на наличие метода приходится писать гирлянды


class ObjectV2: public ObjectV1 
{ public: virtual void new_run(); };

class ObjectV3: public ObjectV1 
{ public: virtual void new_run(); };

...

class ObjectVn: public ObjectV1 
{ public: virtual void new_run(); };


try {
    dynamic_cast<ObjectV2 *>obj->new_run();
} except {
try {
    dynamic_cast<ObjectV3 *>obj->new_run();
} except {
try {
    dynamic_cast<ObjectV4 *>obj->new_run();
} except {
try {
...
} except {
    dynamic_cast<ObjectV1 *>obj->run();
}}}} .... }}}};

monk ★★★★★
() автор топика
Ответ на: комментарий от anonymous

заканчивая отсутствием гарантии вызова - да, да - финализатор могут вообще не вызвать

А можно ссылочку на такой факт? Это в какой реализации?

monk ★★★★★
() автор топика
Ответ на: комментарий от anonymous

начиная с того, что полей уничтожаемого объекта может уже давно не существовать

для Racket не может: http://docs.racket-lang.org/reference/willexecutor.html#(def._((quote._~23~25...

GC весьма вредная зараза, распростроняющаяся на всю архитектуру

Вообще-то во всех архитектурах с GC есть FFI с malloc/free. Так что если очень хочется, то можешь аллоцировать и освобождать вручную.

monk ★★★★★
() автор топика
Ответ на: комментарий от mv

CL - это те три экрана кода, в которые минимальный REPL умещается.

чушь, и ты это знаешь, чтоб построить сверху тот же CLOS, или что угодно другое, не требующее расширения реализации ЯП - надо гораздо больше кода

wota ★★
()
Ответ на: комментарий от monk

Именно в таком случае помогает dynamic_cast,

Просто в крестах много ручного управления. Если надо в динамике проверять какое то свойство, например наличие метода, то:


class Object {
public: 
virtual void run();
virtual void new_run();
virtual bool have_new_run();
};

class ObjectV1: public Object 
{ public: virtual void run(); };

class ObjectV2: public ObjectV1 
{ public: virtual void new_run(); 
virtual bool have_new_run();};

class ObjectV3: public ObjectV1 
{ public: virtual void new_run(); 
virtual bool have_new_run();};

...

class ObjectVn: public ObjectV1 
{ public: virtual void new_run(); 
virtual bool have_new_run();};

...

if (obj->have_new_run()) obj->new_run(); else obj->run();
no-such-file ★★★★★
()
Ответ на: комментарий от no-such-file

А вы как определяете, что надстройка, а что нет?

то, что (может быть) реализовано средствами самого ЯП - надстройка, например в плюсах «умные» указатели - надстройка, а классы - нет

Ну так там сплошная динамика - за все надо платить.

вот в этом и разница, в той же Java ООП не «статичное» как в плюсах, и не надстройка как в CL - потому и очень быстрое и «динамичное»

wota ★★
()
Последнее исправление: wota (всего исправлений: 1)
Ответ на: комментарий от no-such-file

Просто в крестах много ручного управления. Если надо в динамике проверять какое то свойство, например наличие метода, то:

struct I_ObjectWithNewRun
class ObjectV2: public ObjectV1, public I_ObjectWithNewRun
wota ★★
()
Последнее исправление: wota (всего исправлений: 1)

Хаскель - это другое. А вот, «идеальной реализации ООП» в природе не существует.

Реализации ООП грубо делятся на два типа:

  • Вызов метода - это передача сообщения объекту (Simula (??), Smalltalk и подобное);
  • Вызов метода(ов) обобщенной функции с диспетчеризацией по параметрам (CLOS из Common Lisp).

У каждого подхода есть плюсы и минусы. Идеала нет. Универсальных методов решения любых задач тоже нет. И не только в программировании.

dave ★★★★★
()
Последнее исправление: dave (всего исправлений: 1)
Ответ на: комментарий от wota

public I_ObjectWithNewRun

Это как раз есть «правильный путь» по C++/Java. Вот только к исходному «методы и сообщения одно и то же; проверить наличие произвольного метода можно и в C++»/ оно уже не относится.

monk ★★★★★
() автор топика
Ответ на: комментарий от dave

на два типа:

Третий тип — C++ и Java. Там метод = функция, принадлежая классу.

Четвёртый — GObject. Метод = функция, принадлежащая объекту. В момент инстанцирования методы по-умолчанию берутся из класса.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Си++ и Java - первый тип. Внешнее оформление не играет роли.

dave ★★★★★
()
Ответ на: комментарий от monk

Тут может быть много критериев для классификации. Я использовал критерий по вызову метода. Ты же - по принадлежности к классу и пространству имен. Твой критерий - не самое главное в ООП. Кстати, в Smalltalk методы определяются как часть класса, и их названия принадлежат пространству имен, образуемых классом. Здесь нет отличия от Java или Си++.

К тому же, принадлежность к классу всегда можно обозначить, например, в лиспе, добавкой к имени метода. Необязательно брать все имя класса целиком - достаточно бывает взять характеристическую часть имени.

dave ★★★★★
()
Ответ на: комментарий от dave

То есть у тебя критерий: динамическая диспетчеризация по одному параметру или нескольким. Причём, если по одному, то это передача сообщений.

А куда ты отнесёшь такого монстра, который позволяет такую семантику:

(defmessage class-a (:foo (i class-b))
   (call-with-a-and-b class-a i))

(defmessage class-a (:foo (i class-a))
   (call-with-a-and-a class-a i))

(defmessage class-a (:foo (i class-a) :at (j class-b))
   (call-with-a-a-and-b class-a i j))

(let ((a (make-instance 'a))
      (b (make-instance 'b)))
  (@ a :foo b) ; call-with-a-and-b
  (@ a :foo a) ; call-with-a-and-a
  (@ a :foo a :at b))  ; call-with-a-a-and-b

Здесь мультиметоды на сообщениях.

monk ★★★★★
() автор топика
Ответ на: комментарий от wota

то, что (может быть) реализовано средствами самого ЯП - надстройка

В отношении лиспа это не применимо же.

вот в этом и разница, в той же Java ООП не «статичное» как в плюсах, и не надстройка как в CL - потому и очень быстрое и «динамичное»

Java

очень быстрое и «динамичное»

/0

no-such-file ★★★★★
()
Ответ на: комментарий от wota

class ObjectV2: public ObjectV1, public I_ObjectWithNewRun

И дальше что? Можно на шаблонах сделать проверялку, наследуется ли класс от I_ObjectWithNewRun, но дело то в том, что выбор метода будет работать только на этапе компиляции, а не в динамике. Т.е.

if (условие) obj->new_run(); else obj->run();

если obj не содержит new_run (не унаследовано от ObjectWithNewRun) не скомпилируется.

no-such-file ★★★★★
()
Ответ на: комментарий от monk

Это то, без чего я как раз советовал обойтись: один dynamic_cast уже тормозит, а вместе с try - это вообще жесть.

no-such-file ★★★★★
()
Ответ на: комментарий от no-such-file

один dynamic_cast уже тормозит, а вместе с try - это вообще жесть.

try там не нужен, а во-вторых:

~$ cat 1.cpp
#include <cstdio>

struct Object { virtual int run() { return 0; } };
struct I_ObjectWithNewRun { virtual int new_run() = 0; };
struct Object2 : Object, I_ObjectWithNewRun { int new_run() { return 1; } };

int main()
{
	int sum = 0;

	Object* o = new Object2;
	for( int i = 0 ; i < 10000000 ; ++i )
	{
		I_ObjectWithNewRun* o2 = dynamic_cast<I_ObjectWithNewRun*>( o );
		sum += o2 ? o2->new_run() : o->run();
	}

	printf( "%d\n", sum );
}
~$ g++ -Ofast 1.cpp && time ./a.out
10000000

real	0m0.282s
user	0m0.280s
sys	0m0.000s

всем бы так «тормозить»

wota ★★
()
Ответ на: комментарий от wota

С кастом:

$ cat 1.cpp 
#include <cstdio>

struct Object {ivirtual int run() { return 0; } };
struct I_ObjectWithNewRun { virtual int new_run() = 0; };
struct Object2 : Object, I_ObjectWithNewRun { int new_run() { return 1; } };

int main()
{
        int sum = 0;

        Object* o = new Object2;
        for( int i = 0 ; i < 100000000 ; ++i )
        {
                I_ObjectWithNewRun* o2 = dynamic_cast<I_ObjectWithNewRun*>( o );
                sum += o2 ? o2->new_run() : o->run();
        }

        printf( "%d\n", sum );
}

$ g++ -Ofast 1.cpp && time -p ./a.out
100000000
real 7.35
user 7.33
sys 0.00

Без каста:


$ cat 2.cpp 
#include <cstdio>

struct Object { virtual int run() { return 0; } };
struct I_ObjectWithNewRun { virtual int new_run() = 0; };
struct Object2 : Object, I_ObjectWithNewRun { int new_run() { return 1; } };

int main()
{
        int sum = 0;

        Object2* o = new Object2;
        for( int i = 0 ; i < 100000000 ; ++i )
        {
        sum += o->new_run();
        }

        printf( "%d\n", sum );
}

$ g++ -Ofast 2.cpp && time -p ./a.out
100000000
real 0.23
user 0.23
sys 0.00

С виртуальными функциями:

$ cat 3.cpp
#include <cstdio>

struct Object { 
        virtual int run() { return 0; }
        virtual int new_run() { return 0; }
        virtual bool have_nr() { return false; }
};

struct Object2 : Object{ 
        int new_run() { return 1; } 
        bool have_nr() { return true; } 
};

int main()
{
        int sum = 0;

        Object* o = new Object2;
        for( int i = 0 ; i < 100000000 ; ++i )
        {
                sum += o->have_nr() ? o->new_run() : o->run();
        }

        printf( "%d\n", sum );
}

$ g++ -Ofast 3.cpp && time -p ./a.out
100000000
real 0.39
user 0.39
sys 0.00

С кастом в 32 раза медленнее чем безусловный вызов. Это я и называю «тормоза».

no-such-file ★★★★★
()
Ответ на: комментарий от no-such-file

Мультиметоды на сахаре

Семантически это сообщения. Начиная от поведения при получении неизвестного сообщения, заканчивая тем, что имена сообщений никак не пересекаются с именами функций. Минус --- труднее запихивать в комбинаторы.

Получатель сообщения имеет право проанализировать параметры и провести диспатч по типам :-)

monk ★★★★★
() автор топика
Ответ на: комментарий от no-such-file

Никогда не понимал, почему dynamic_cast не может работать быстрее. К моменту создания класса вся вышестоящая иерархия ему доступна - можно просто хранить список id-типов к которым можно кастовать и соответствующее смещение. Не?

anonymous
()
Ответ на: комментарий от monk

Можно это на нормальном языке было написать? Что ты тут делаешь? Диспетчеризируешь еще и по параметру метода?

anonymous
()
Ответ на: комментарий от anonymous

Можно это на нормальном языке было написать

Наверное можно. Синтаксис только придумать надо.

Диспетчеризируешь еще и по параметру метода?

Да. Можно указывать класс либо значение соответсвующего параметра. Причём у одного сообщения может быть несколько методов с разным числом параметров (параметры именованные в стиле Smalltalk).

monk ★★★★★
() автор топика
Ответ на: комментарий от no-such-file

С кастом в 32 раза медленнее чем безусловный вызов. Это я и называю «тормоза».

ну то, что прямой вызов быстрее - это очевидно, вот только код:

struct Object { 
        virtual int run() { return 0; }
        virtual int new_run() { return 0; }
        virtual bool have_nr() { return false; }
};

в реальной жизни не имеет смысла, если у нас legacy - мы не можем так просто изменить базовый класс, только добавить новый, а если можем - то можем сразу и во всех наследниках изменить run и обойтись без new_run

wota ★★
()
Ответ на: комментарий от anonymous

К моменту создания класса вся вышестоящая иерархия ему доступна - можно просто хранить список id-типов к которым можно кастовать и соответствующее смещение. Не?

a) не вся - есть еще библиотеки
б) множественное наследование

wota ★★
()
Последнее исправление: wota (всего исправлений: 1)
Ответ на: комментарий от monk

Это скорее паттерн матчинг по типам аргументов сообщения. Это не мультиметоды. Хотя бы потому, что поведение целиком и полностью описывает класс A.

anonymous
()
Ответ на: комментарий от wota

если у нас legacy - мы не можем так просто изменить базовый класс

Если у нас legacy без исходников - то не надо его насиловать.

а если можем - то можем сразу и во всех наследниках изменить run и обойтись без new_run

Вопрос не в том как на крестах пишут или надо писать, в в том, можно ли писать так, как просил monk (т.е. проверять наличие метода в классе в рантайме).

no-such-file ★★★★★
()
Ответ на: комментарий от anonymous

Библиотеки не могут добавить предков.

а dynamic_cast и не нужен, чтоб кастовать «вниз», он используется для каста «вбок» и «вверх», и там кроме всего прочего надо не только вычислить «путь» для каста, но и проверить - а возможен ли он в принципе, так как указатель на руках совсем не обязательно является экземпляром нужного класса

wota ★★
()
Ответ на: комментарий от no-such-file

в в том, можно ли писать так, как просил monk (т.е. проверять наличие метода в классе в рантайме

и этот вопрос явно противоречит этой строке:

virtual int new_run() { return 0; }
wota ★★
()
Ответ на: комментарий от wota

но противоречит этой строке:

Нет не противоречит. То что объект «знает», что такой метод бывает, не значит что он есть (реализован).

С помощью dynamic_cast можно проверять наличие интерфейса, о котором базовый объект не знает, да. Но это требует больших издержек. Вас что именно не устраивает?

no-such-file ★★★★★
()
Ответ на: комментарий от wota

Пример, пожалуйста. Я же не о статическом списке говорю. Список хранится где-нить рядом с vtbl.

anonymous
()
Ответ на: комментарий от anonymous

самый простой пример, даже без множественного наследования, виртуального и т.п.:

A->B B->C

как у тебя указатель на A будет кастоваться к С?

wota ★★
()
Ответ на: комментарий от no-such-file

Вас что именно не устраивает?

«в том, можно ли писать так, как просил monk (т.е. проверять наличие метода в классе в рантайме)» (c), QObject к примеру такое позволяет, твое решение - совсем другая задача

wota ★★
()
Ответ на: комментарий от wota

а тебе с этим жить дальше ;)

А мне то что, пойду День Радио отмечать.

no-such-file ★★★★★
()
Ответ на: комментарий от wota

Стрелчки у тебя в сторону базового класса? Тогда при формировании B в список вносится id C и соответствующее смещение(0), при формировании A, в его список заносится список базового класса(т.е. B, а значит (id C, 0)) + инфа о базовом классе(B, т.е. (id B, 0)). При касте ищем id(пусть даже линейно, а можно по хешу) - никак в 36 раз медленнее простого вызова не будет... Интересно, как dynamic_cast в том же gcc реализован.

anonymous
()
Ответ на: комментарий от anonymous

Стрелчки у тебя в сторону базового класса?

B наследуется от A, C от B

При касте ищем id(пусть даже линейно, а можно по хешу) - никак в 36 раз медленнее простого вызова не будет...

для каста в обратную сторону (как ты видимо понял) вообще нет динамики - это делает компилятор и тут хватит static_cast, а для каста вверх нельзя взять и добавить смещение, т.к. A* подразумевает под собой любой класс, который наследован от A, он может быть экземпляром C, а может и нет, а может у C несколько родительских A и/или B и т.д., все это надо проверять и узнавать в динамике

wota ★★
()
Последнее исправление: wota (всего исправлений: 1)
Ответ на: комментарий от wota

Так я про динамику и говорю. Но она почему-то слишком тормозная для такой простой задачи...

anonymous
()

Когда использую сишку, делаю обычно просто структурки с указателями на функции, можно назвать это «интерфейсами», с другой стороны, примерно так и работают под капотом haskell'евские тайпклассы. Мультиметоды, ИМХО, малополезны.

А в более-менее больших проектах предпочитаю С++ ООП. Минимум оверхеда при достаточно хорошем наборе возможностей.

Haskell все-таки в списке лишний...

forCe
()
Ответ на: комментарий от forCe

Структурки с указателями на функции - это еще не ООП. Хотя с точки зрения практики решения задач - самое то... Нужно ли «классическое» ООП - вопрос открытый. Очень часто это похоже больше на религию, чем на практику программирования...

anonymous
()
Ответ на: комментарий от anonymous

Структурки с указателями на функции - это еще не ООП

Вообще-то, ООП, т.к. эти структурки и есть объекты, а большего ООП по определению не требует.

no-such-file ★★★★★
()
Ответ на: комментарий от no-such-file

Ну в Си такие структуры обычно отделены от самих данных. Если, конечно, речь не о каком-то обобщенном механизме вроде gobject.

anonymous
()

ООП. Как представляете себе идеальную реализацию?

Например, стрелки. Единственный вариант, который я пока вижу. Нужно просто-типизированное LC это будет одна конструкция из стрелок/2-стрелок, нужно LC с зависимыми типами - другая, с субструктурными (линейными, в частности) - ещё третья. Т.е. фиксированное подмножество теории категорий само по себе довольно бедная метатеория, просто язык, в котором можно выразить все эти вещи. Мартин-Лёф предлагал формулировать разные теории типов в рамках метатеории LF, вот также это можно делать в рамках метатеории ТК.

Ну, например, берём декартово-замкнутую категорию Set, она же классический топос, т.е. категория всех малых множеств и тотальных функций, в agda она так и называется - «Set». Берём категорию всех малых категорий и функторов Cat (Set1 в agda) и утверждаем:

  • Любой тип который может использоваться (ADTs, GADTs, Inductive families) это объект Set (объект топоса, в общем случае), т.е., как следствие, множество - множество всех термов данного типа.
  • Любой параметрически-полиморфный тип это эндофунктор на Set, т.е. специального вида стрелка в Cat. (Непараметрический тип, который объект в Set, это вырожденный случай функтора из терминальной категории (как-то так)).
  • Любой конструктор типа данных который может быть это стрелка в Set (стрелка в топосе, вообще).
  • Любая функция которая может быть это тоже стрелка в Set, но отдельным классом (чтобы не путать стрелки-конструкторы и стрелки-функции). Как следствие, «функция» - тотальная функция между множествами.
  • Любая композиция стрелок с учётом декартовых произведений и экспоненциалов это любой правильно составленный терм в данной системе (тут типизация из коробки).
  • Любое определение конкретной редукции это 2-стрелка (струнная диаграмма).
  • Любой конкретный ход редукций (вычислений) это композиции 2-стрелок (струнных диаграмм). + Правила построения редукций из коробки.

Например:

-- Тут рисуем коммутативную диаграмму:
ℕ : Set
0 : 1 → ℕ
s : ℕ → ℕ

-- Продолжаем коммутативную диаграмму:
+ : ℕ → (ℕ → ℕ)

-- Рисуем две струнных диаграммы, которые можно сочетать:
e₁(+) : + ∘ a ∘ 0 ⇒ a
e₂(+) : + ∘ a ∘ (s ∘ b) ⇒ s ∘ (+ ∘ a ∘ b)

и это просто ТК (можно представить формальный синтаксис такого языка), безотносительно какого-либо ЯП, но понятно из каких ADT и функции это получилось.

Тут ещё интересно, что можно легко добавлять конструкторы и правила редукций (как если бы в хаскеле можно было дописывать ADT и pattern matching cases по разным модулям).

Сами по себе 2-стрелки это непосредственно струнные диаграммы, т.е., рисуя коммутативные диаграммы, получим схемы типов, рисуя струнные - flow chart.

  • Любая конкретная оптимизация это 3-стрелка. Правила построения оптимизаций тоже из коробки.

Например, если есть линейная рекурсия:

f x = z
f y = g (f (h y))

(f не появляется в g и h), т.е.:

-- любая стрелка вида:
f : x + y → r

-- с 2-стрелками вида:
e₁(f) : f ∘ x ⇒ z
e₂(f) : f ∘ y ⇒ g ∘ f ∘ h ∘ y

то она убирается такой 3-стрелкой:

-- Рисуем диаграмму между струнными:
o(f, f′) : e(f) ≡> e(f′ ∘ z)

-- TCO форма:
e₁(f′) : f′ ∘ a ∘ x ⇒ a
e₂(f′) : f′ ∘ a ∘ y ⇒ f′ ∘ (g ∘ a) ∘ (h ∘ y)

и остаётся только TCO.

Процесс унификации по 3-стрелкам это процесс оптимизаций, а процесс унификации по 2-стрелкам - интерпретации или компиляции (тогда нужен target). Как компилировать в target - конструкторы, например, достаточно точно отражаются в си-подобные структуры и объединения в памяти, можно даже в случае (s : ℕ → ℕ) или (_:_ : a → [a] → [a]) или вообще (con : [no `t' appears] → t → t) пытаться делать не связные списки, а аллоцируемые/реаллоцируемые массивы. Про компиляцию редукций ничего не скажу - только, наверно, тут, в первую очередь, нужен критерий линейности представляемого терма и/или его линеаризация.

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.