LINUX.ORG.RU

Повторное использование кода

 , ,


1

2

Как определяется, в какую сторону должно быть обобщение?

То есть, при решении конкретной задачи как определить, что должно быть параметром, а что — неизменяемой частью.

Например, есть задача «получить сумму от 1 до 100».

Самым быстрым решением будет

class Sum1_100
{
  int run() { return 5050; }
}

но в этом случае программист поработал за компьютер. Тривиальное решение с расчётом компьютером

class Sum1_100
{
  int run() 
  { 
    int res = 0; 
    for(int i=1; i<=100; i++) res+=i; 
    return res; 
  }
}

Но получив такую задачу, почти всегда решение выглядит примерно так:

class Sum1_n
{
  int n;
  Sum1_n(int _n = 100) { n = _n; };
  int run() 
  { 
    int res = 0; 
    for(int i=1; i<=n; i++) res+=i; 
    return res; 
  }
}

на случай, если потребуется не до 100, а до какого-то другого числа.

И вот здесь у меня вопрос: почему в «получить сумму от 1 до 100» большинство параметризуют именно 100? Посему не «сумму» (тогда параметром будет операция от 100 элементов) или не «от 1 до 100» (тогда параметром будет некая последовательность). Или вообще не всё сразу? С вызовом типа

  auto_ptr<Op> op = new GenOp(operator+, 100);
  auto_ptr<Seq> seq = new Seq(1, 100);
  auto_ptr<Apply> = new GenApply(op, seq);
  result = Main->run();

Как для произвольного алгоритма определяется, что является параметром, а что неизменяемой частью?

★★★★★

Последнее исправление: monk (всего исправлений: 2)

Как для произвольного алгоритма определяется, что является параметром, а что неизменяемой частью?

Каждый конкретный алгоритм (например имени кого-то там) описывает решения задачи, бывшей проблеммой (по производительности или др.) и стало быть содержит какую-то замысловатость. У решения есть рамки (напр. + нельзя заменить на * в формуле суммы арифметической-геометрической прогрессии), все остальное можно запараметризовать (~ числа в некоторых рамках). Реально и этого,оставшегося слишком много. Если все запараметризовать получается неподъемное, недоделанное говно (типа ГНУ автоконф. смаке(>=3) ну и дальше до бесконечности). А про замену операции - это только на бумажке, теория групп напр. (gap - вещь в себе. Даже в максиме брать производную по любой букве в формуле - как-то костыльно выходит)

Короче, какая задача - то и параметризуем.

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

Это всё верно. Но почему же тогда в Go и Rust его выкинули (вместе с наследованием)?

Из Rust его никто не выкидывал. В Rust любой экземпляр типа над которым реализован трейт(класс типов) `T` можно кастануть в trait object `&T` и после запихнуть trait object'ы, которые кастанули от разных типов в любой контейнер над `&T`.

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

Иначе обобщённый код почти гарантированно проигрывает (если очень умный компилятор всё не заинлайнит).

Неверно. В большинстве случаев обобщённый(полиморфный) код мономорфизируется на этапе компиляции, разве что для JIT компиляции требуется некоторое время для «прогрева».

Nexmean
()

То есть, при решении конкретной задачи как определить, что должно быть параметром, а что — неизменяемой частью.

В рамках прикладного кода эта задача решается очень просто. Почитайте Кента Бэка. Сначала пишем тесты, потом пишем код и только потом устраняем дублирование. Иначе говоря дублирование кода необходимо устранять после его появления, в таком случае, лишние человеко-часы не будут потрачены на изобретение и поддержку никому не нужных абстракций(и на войну с недостатком охвата юзкейсов этих абстракций) и дублирование кода будет устраняться по мере необходимости.

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

использовать вместо него (пока несуществующую) функцию, которая создаёт строку заданной длины заполняя её переданной строкой = изменить алгоритм

Да, но не алгоритм функции leftPad, в нём ничего трогать не нужно. Собственно ради этого и весь сыр-бор. Повторное использование кода.

Достаточно слова в Библии переопределить, чтобы они значили не то, что значат всегда

Так не получится. Т.к. для это по факту придётся заменить Библию другой книгой-толкователем. Вот если бы можно было заменить смысл отдельных слов и вместо Библии получился бы учебник физики - тогда твой аргумент был бы в тему.

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

Всегда ли нужна максимально возможная декомпозиция и абстракция?

Да.

есть ли объективный критерий её нужности

Возможность её реализовать на конкретном этапе разработки. В основном ограничивающий фактор - это время, отведённое на реализацию. Гораздо реже - скорость работы данной реализации, т.к. абстракции обычно не бесплатны.

Вроде бы это очевидно, не?

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

Но почему же тогда в Go и Rust его выкинули (вместе с наследованием)?

Кого выкинули? LSP? Не выкинули, с чего ты это взял?

Вполне успешный MacOS, сменивший весь API несколько раз. …

Наверняка в течение не одного месяца и достаточно плавно, предоставляя пользователям более-менее достаточное время для апдейта?

JDK, например, достаточно долго сохраняет совместимость, и, хотя у этого есть свои минусы (типа распухания легаси-кодом), это позволяет не трогать работающий код годами. Или трогать, но только для фиксов/улучшений, а не для адаптации под новую версию JDK/JRE, что довольно непродуктивное занятие.

korvin_ ★★★★★
()
Ответ на: Smalltalk от anonymous

как с этими проблемами работали в Smalltalk?

Какими проблемами? Anyway, спроси yoghurt'а, он знаток SmallTalk'а.

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

Какими проблемами?

т.е. обозначенное для LSK и OCP — не проблемы построения сложных иерархических систем?

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

В большинстве случаев обобщённый(полиморфный) код мономорфизируется на этапе компиляции

Ну-ну. Попробуй посмотреть выхлоп компилятора на

 int run() 
  { 
    int res = 0; 
    for(int i=1; i<=100; i++) res+=i; 
    return res; 
  };

и

  int run1() 
  { 
    std::list<int> x(100);
    std::iota(x.begin(), x.end(), 1);
    return std::accumulate(x.begin(), x.end(), 0);
  }  

Первое схлопывается до return 5050, а второе раскладывается в честный цикл, так как компилятор недостаточно умён.

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

В Rust любой экземпляр типа над которым реализован трейт(класс типов) `T` можно кастануть в trait object `&T` и после запихнуть trait object'ы, которые кастанули от разных типов в любой контейнер над `&T`.

Что-то слишком заумно. Вот есть у меня

struct Point {
    x: f32,
    y: f32,
}

fn init_point(p : Point) { .. }

Как мне сделать coloredPoint c дополнительным полем color, чтобы значения этого типа модно было передавать в init_point?

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

Кого выкинули? LSP? Не выкинули, с чего ты это взял?

Так если нет наследования, то нет и LSP. Реализация интерфейса — это уже другое.

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

JDK, например, достаточно долго сохраняет совместимость

А .Net нет. И в MS Windows стал основной платформой.

это позволяет не трогать работающий код годами.

В случае .Net просто приходится ставить все версии .Net рядом.

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

Реализация интерфейса — это уже другое

То же самое. LSP не требует наследования. LSP говорит о типе и подтипе. Реализация интерфейса - это подтип интерфейса.

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

LSP говорит о типе и подтипе.

да

Реализация интерфейса - это подтип интерфейса.

Интерфейс не является типом.

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

т.е. обозначенное для LSK и OCP — не проблемы построения сложных иерархических систем?

LSK нарушается в стандартной библиотеке. CBoolean является подтипом CByte, но метод value возвращает boolean вместо числа.

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

Интерфейс не является типом

Для CS является. Как там в конкретном языке сделано, и есть ли там вообще «интерфейсы» как отдельные сущности, глубоко насрать.

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

т.е. обозначенное для LSK и OCP — не проблемы построения сложных иерархических систем?

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

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

Для CS является.

У типа должны быть значения, данные принадлежащие этому типу. Ты не можешь создать объект имеющий тип интерфейса. Интерфейс — это контракт на создаваемый тип, а не сам тип.

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

Так если нет наследования, то нет и LSP

Чушь, LSP не про наследование. LSP про субтипирование, субтипирование != наследование. В TAPL, например, про субтипирование есть.

Реализация интерфейса — это уже другое.

В каком месте это другое? LSP про субтипирование, как там оно реализовано — дело другое.

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

А .Net нет. И в MS Windows стал основной платформой.

Не удивительно. Но к чему это?

В случае .Net просто приходится ставить все версии .Net рядом.

Т.е. разработчики «прикладного софта» выбирают держать стабильное API, под которое написан их софт, вместо перепиливания своего софта под более актуальные версии .NET? О чём и речь.

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

Интерфейс не является типом.

ORLY? А чем же он является? С точки зрения всех систем типов во всех языках, где есть интерфейсы, они (интерфейсы) являются типами.

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

У типа должны быть значения, данные принадлежащие этому типу.

Не обязательно.

Ты не можешь создать объект имеющий тип интерфейса.

Могу:

Interface foo = new Implementation();

Интерфейс — это контракт на создаваемый тип, а не сам тип.

Ты путаешь понятия «тип» и «структура данных». Тип не обязан описывать структуру данных, особенно абстрактный тип, который описывает только операции над значениями типа. Реализация этих значений и операций — дело десятое.

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

Т.е. разработчики «прикладного софта» выбирают держать стабильное API, под которое написан их софт, вместо перепиливания своего софта под более актуальные версии .NET? О чём и речь.

Новые версии софта пишут под новые версии .NET. И даже старые переписывают. Много в линуксе найдёшь программ, которые до сих пор используют стабильное API GTK1 и Qt1?

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

Interface foo = new Implementation();

А если считать, что это тип, то принцип подстановки нарушается автоматически, так как

interface Interface ...

interface InterfaceChild extends Interface ...

class Implementation implements Interface ...

InterfaceChild foo = new Implementation();

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

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

Если ты сделаешь тип `ColoredPoint`, то он не будет подтипом `Point`. С другой стороны ты можешь сделать `trait Point`,

trait Point {
    fn x(&self) -> f32;

    fn y(&self) -> f32;

    fn setX(&mut self, x: f32);

    fn setY(&mut self, y: f32); 
}
реализовать этот трейт для нескольких типов и через каст к трейт объекту запихнуть их в, например, массив.

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

Тогда лучший язык — APL.

То есть ты сам упомянул архивацию, которая сотрёт характеристику длины токена, и всё равно решил шуткануть языком, о котором знаешь только про нестандартную нотацию.

Нет, APL не оказался лучшим, однако в своей области предназначения его подход оказался общеупотребимым, и у текущего мэинстрима его возможности уже ассимилированы (OLAP-кубы, R, векторные операции в matlab/fortran, python numpy)

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

реализовать этот трейт для нескольких типов

Это я и имел в виду, что нет наследования. Фактически, при написании программы приходится явно указывать, что «а здесь возможно не только объект, но и любой его потомок». А если, например, http://gtk-rs.org/docs/gtk/trait.WidgetExt.html#tymethod.register_window хочет структуру gdk::Window, то передать туда ссылку на потомка (как можно даже в Си) уже не получится.

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

То есть ты сам упомянул архивацию, которая сотрёт характеристику длины токена, и всё равно решил шуткануть языком, о котором знаешь только про нестандартную нотацию.

Попробуй любым архиватором упаковать код на Java для игры Life, чтобы вмещалась в 66 байт:

life←{↑1 ⍵∨.∧3 4=+/,¯1 0 1∘.⊖¯1 0 1∘.⌽⊂⍵}
monk ★★★★★
() автор топика
Ответ на: комментарий от aedeph_

которая сотрёт характеристику длины токена

Там ещё и количество токенов, как правило, сильно меньше в реализации.

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

И при чтении типа значения foo.getClass().getName()) вернёт тебе «Implementation».

И? При наследовании от какого-нибудь базового класса BasicImplementation вместо Interface, foo.getClass() так же вернул бы Implementation.class.

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

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

А с чего бы ему компилироваться? Implementation будет субтипом Interface, но не InterfaceChild. При наследовании классов всё происходит ровно так же. При любой субтипизации происходит так же. Оно и не должно компилироваться.

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

И? При наследовании от какого-нибудь базового класса BasicImplementation вместо Interface, foo.getClass() так же вернул бы Implementation.class.

Верно. Потому что тип объекта — Implementation. Но для базового класса BasicImplementation создать объект этого типа можно, а для интерфейса можно только описать тип-реализацию.

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

А с чего бы ему компилироваться?

«Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.» (с) определение LSK. Попытка заменить Interface на InterfaceChild приводит к ошибке компиляции.

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

Верно. Потому что тип объекта — Implementation. Но для базового класса BasicImplementation создать объект этого типа можно, а для интерфейса можно только описать тип-реализацию.

И что? https://en.wikipedia.org/wiki/Type_system#Existential_types

Любое значение типа-реализации интерфейса будет являться значением этого типа-интерфейса.

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

«Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.» (с) определение LSK.

Нет такого определения. Есть такое: «in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e. an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of T (correctness, task performed, etc.)»

Попытка заменить Interface на InterfaceChild приводит к ошибке компиляции.

Потому что LSP не про замену типов, а про замену значений подтипа в месте, где ожидается супертип этого подтипа.

И правильно приводит, т.к. Implementation не является подтипом InterfaceChild. Ты не подставляешь значение типа S (являющегося подтипом T) в метод, ожидающий значение типа T, а меняешь T на T', подтипом которого S не является. LSP тут вообще не при чём.

Даже по твоему определению: есть функция (var T = val T) :: T -> () ожидающая T для val, вместо объекта типа T подставляем объект типа S = Implementation <: T — всё работает, т.к. S является субтипом T. Меняем сигнатуру на (var T' = val T') :: T' -> (), пробуем снова подставить S, ничего не работает, т.к. S не является подтипом T'. Всё логично.

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

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

#lang racket

(define-syntax-rule (log form ...)
  (begin
    (with-handlers ((exn:fail?
                     (lambda (e) (printf "~nERROR: ~s~n~a~n~N" 'form e))))
      (printf "~s ;=> ~s~n" 'form form))
    ...))

(define inspector (make-inspector))

(define (class-of object)
  (let-values (((class skipped?) (object-info object))) class))

(define Interface<%> (interface () foo))

(define Interface-Child<%> (interface (Interface<%>) bar))

(define Implementation%
  (class* object% (Interface<%>)
    (inspect inspector)
    (super-new)
    (define/public (foo)
      "Impl::Foo")
    ))

(define (try-interfaces)
  (define obj (new Implementation%))
  (log
   (class-of obj)
   (is-a? obj Interface<%>)
   (is-a? obj Interface-Child<%>)
   (send obj foo)
   (send obj bar)))

(define Parent%
  (class object%
    (inspect inspector)
    (super-new)
    (define/public (foo)
      "Parent::foo")
    ))

(define Child%
  (class Parent%
    (inspect inspector)
    (super-new)
    (define/public (bar)
      "Child::bar")
    ))

(define Impl%
  (class Parent%
    (inspect inspector)
    (super-new)
    (define/override (foo)
      "Impl::foo")
    ))

(define (try-classes)
  (define obj (new Impl%))
  (log
   (class-of obj)
   (is-a? obj Parent%)
   (is-a? obj Child%)
   (send obj foo)
   (send obj bar)))

(try-interfaces)
(try-classes)

Что мы видим?

(class-of obj) ;=> #<class:Implementation%>
(is-a? obj Interface<%>) ;=> #t
(is-a? obj Interface-Child<%>) ;=> #f
(send obj foo) ;=> "Impl::Foo"

ERROR: (send obj bar)
#(struct:exn:fail:object send: no such method
  method name: bar
  class name: Implementation% #<continuation-mark-set> ...)

(class-of obj) ;=> #<class:Impl%>
(is-a? obj Parent%) ;=> #t
(is-a? obj Child%) ;=> #f
(send obj foo) ;=> "Impl::foo"

ERROR: (send obj bar)
#(struct:exn:fail:object send: no such method
  method name: bar
  class name: Impl% #<continuation-mark-set> ...)

Какие выводы можно сделать?

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

Нет такого определения.

В википедии написано, что Мартин (автор SOLID) формулировал его именно так.

Вот в оригинале: FUNCTIONS THAT USE POINTERS OR REFERENCES TO BASE CLASSES MUST BE ABLE TO USE OBJECTS OF DERIVED CLASSES WITHOUT KNOWING IT.

Впрочем, ты меня убедил. Указатель на интерфейс в типах функций и контейнеров существует, так что можно их рассматривать как абстрактные классы.

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

В википедии написано

В рукипедии?

Вот в оригинале: FUNCTIONS THAT USE POINTERS OR REFERENCES TO BASE CLASSES MUST BE ABLE TO USE OBJECTS OF DERIVED CLASSES WITHOUT KNOWING IT.

Это означает ровно то, что я написал, т.е. функция

foo : *Interface -> ()

может использовать в качестве аргумента объект типа Implementation, потому что Implementation является субтипом Interface.

Но функция

foo : *InterfaceChild -> ()

не может использовать объект типа Implementation, потому что Implementation не является подтипом InterfaceChild.

LSP про подтипы, а не про любые рандомные типы.

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