LINUX.ORG.RU

Два подхода к контекстам

 , ,


4

2

В этом посте я собираюсь рассмотреть различия в объектной модели Smalltalk и CLOS и как эти модели связаны с понятием контекста. Поклонники статической типизации могут не читать. С открытием CLOS возникли споры о том, CLOS — это что? ООП или не ООП? Становление новой науки неизбежно приводит к терминологическим спорам. ООП — термин расплывчатый и ИМХО, его следовало бы избегать. Как CLOS, так и Smalltalk реализуют одну важную фичу — ad hoc полиморфизм. Эта фича крайне важна для программирования, т.к. позволяет настраивать уже существующий код без изменения его исходного текста. Модель Smalltalk имеет ограниченный ad hoc полиморфизм, т.к. фактически позволяет производить диспетчеризацию лишь по одному аргументу. Однако, кроме ad hoc полиморфизма есть еще одна вещь, связанная с ООП — инкапсуляция. Итак, кратко опишем две ОО модели:

  • Инкапсуляция и ad-hoc полиморфизм (Smalltalk).
  • Ad-hoc полиморфизм без инкапсуляции (CLOS).

Далее я покажу, что эти два подхода противостоят друг другу. В Smalltalk объект — самодостаточная сущность. Чтобы распечатать объект (получить его внешнюю репрезентацию) необходимо послать объекту соответствующее сообщение. Это означает, что внешняя репрезентация объекта зависит только от него, и в минимальной степени зависит от внешнего контекста вызова. В CLOS внешняя репрезентация объекта целиком и полностью зависит от текущей обобщенной функции print-object. Теоретически у одного экземпляра Lisp системы может быть много различных обобщенных функций print-object.

Обычно естественные языки имеют лишь одно текстовое представление. Это не так для иероглифических языков, и скорее всего мы придём со временем к ситуации, когда один и тот же язык будет иметь множество проекций на текст. К этому же идет и эволюция языков программирования. Так, в Perl 6 функция load принимает на вход грамматику, которая описывает Perl 6 и написана на Perl 6. Далее свойство независимости от внешнего представления мы будем называть синтаксической абстракцией. ЯП, наиболее полно поддерживающий синтаксическую абстракцию — Lisp. Программы на Lisp записываются в виде S-выражений, но скобки тут нужны только для того, чтобы указать ридеру структуру. В Lisp текстовая репрезентация программы называются выражениями, а вычислитель работает не с выражениями, а с формами. Термин форма подчеркивает абстрактную природу вычислителя. Я ранее уже писал о том, как можно усилить синтаксическую абстракцию символов в Lisp.

Итак, синтаксическая абстракция предполагает, что объект, помещенный в различные контексты может иметь различные внешние представления. Модель Smalltalk не поддерживает синтаксическую абстракцию, т.к. объект сам должен вернуть свою репрезентацию в ответ на соответствующее сообщение. Давайте теперь обобщим эти рассуждения оторвав их от печати и считывания, на примере которых понятно что, инкапсуляция означает контекстную изоляцию.

Контекст — крайне важное понятие. Игнорирование контекста и абсолютизация понятий приводит к проблемам. Как определить тип объекта? Широко известен спор между Платоном и Диогеном. «Человек, - сказал Платон, - это двуногое животное без перьев». Тогда Диоген ощипал петуха и со словами: «Вот твой человек». Платону пришлось сделать уточнение: «Двуногое животное без перьев и имеющее ногти». Понятно, что тип объекта зависит от наблюдателя. Языки программирования начали с простой идеи — к каждому объекту прилеплен бейджик с его типом. В Smalltalk человеком является тот, кто на вопрос «ты кто?» отвечает — человек. Какой может быть типизация, которая учитывает контекст? Она носит название предикатная типизация. Еще иногда эту идею называют утиной типизацией. В этом подходе тип объекта зависит от того, кто и какие вопросы задает объекту. Платон может определить человека по отсутствию перьев и наличию ногтей, а Диогену нужен фонарь, чтобы определить, кто есть человек.

Одна из наиболее важных идей в истории программирования — гомоиконность является примером переключения контекста. В Lisp нет специально выделенного типа для AST. Является ли определенное дерево AST или нет, зависит от намерений исполнителя. Благодаря этому стало возможным метапрограммирование без значительного усложнения языка. Язык, который выделяет отдельный тип для AST должен иметь весь набор селекторов и конструкторов для этого типа, тогда как представление AST формами дает возможность пользоваться общими селекторами и конструкторами.

Я не отвергаю полностью концепцию инкапсуляции, но хочу обратить внимание, что ее абсолютизация (как в Smalltalk) противоречит некоторым принципам заложенным в динамические языки.

Ответ на: комментарий от J-yes-sir

пистон говно. Это жаба для веба.

Уносите клоуна, пупки трещат!

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

Ну как бы да — наследование в данном случае не образует подтипа, LSP нарушается, и ровно тогда, когда объекты мутабельны, Два подхода к контекстам (комментарий), на Circle x предполагается выполнение контракта окружности, он нарушается при x.scale(1, 2), но контракт предка продолжает выполняться, то есть это как бы эллипс в окружной шкуре уже :) Circle -> Ellipse — это вот про это, с точки зрения обычной теории множеств это всё фигня, конечно.

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

Два подхода к контекстам (комментарий)

Два подхода к контекстам (комментарий)

Речь шла про мутабельные окружности и эллипсы с этой самой «проблемой» и про иммутабельные числа и их подтипы уже без неё. Ну вот если окружности и эллипсы иммутабельны, то есть математичны, то проблема пропадает, а если числа мутабельны (переменные), то та же проблема появляется (Even(...).inc() вместо Circle(...).scale(...)), так что никакой разницы между (математическими или ООП) числами и эллипсами тут нет, всё связанно с семантикой ссылок, их инвариантностью, ковариантностью по чтению и контравариантностью по записи.

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

Ну как бы да — наследование в данном случае не образует подтипа

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

Но если уж мы обсуждаем эти вещи, то когда говорим «у меня тут клсс Эллипс с подтипом Круг», то подразумевается, что они реализованы _корректно_. Ну точно так же, как если мы говорим «и тут у меня ф-я сортировки», то предполагается, что эта ф-я реализована _корректно_. Хотя, повторяю, ее _можно_ реализовать некорректно. То же самое и про Integer/Even.

на Circle x предполагается выполнение контракта окружности, он нарушается при x.scale(1, 2), но контракт предка продолжает выполняться, то есть это как бы эллипс в окружной шкуре уже :)

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

LSP нарушается

LSP вообще нарушается, т.к. запрещает писать предикат, который может вернуть #f на подтипе, когда на супертипе дается #t.

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

Ну вот если окружности и эллипсы иммутабельны, то есть математичны, то проблема пропадает

Да проблемы и нет. Есть неправильная реализация.

то та же проблема появляется (Even(...).inc() вместо Circle(...).scale(...))

А вот для чисел - все по-другому. Само существование inc запрещает считать even подклассом integer. Все потому, что нельзя реализовать inc так, чтобы оно не выводило even из even по определению inc (в отличии от stretch, там все пишется). Но вот если поменять inc на next то уже все хорошо, например.

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

Не вижу в этом никакой проблемы. Выдуманные трудности какие-то.

Я тоже не вижу, но так называется. Если заходит речь об этой проблеме, то подразумевается обычное наследование окружности/квадрата/... от эллипса/прямоугольника/..., а если реализовывать корректно, то и обсуждать нечего.

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

Само существование inc запрещает считать even подклассом integer. Все потому, что нельзя реализовать inc так, чтобы оно не выводило even из even по определению inc (в отличии от stretch, там все пишется). Но вот если поменять inc на next то уже все хорошо, например.

Само существование void Ellipse::scale(Factor, Factor); запрещает считать Circle подклассом Ellipse. Все потому, что нельзя реализовать scale так, чтобы оно (не?) выводило Circle из Circle по определению scale (в отличии от ?, там все пишется). Но вот если поменять void Ellipse::scale(Factor, Factor); на Ellipse Ellipse::scale(Factor, Factor) const; то уже все хорошо, например.

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

Все потому, что нельзя реализовать scale так, чтобы оно (не?) выводило Circle из Circle по определению scale

Как же нельзя? Можно.

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

Тогда scale от двух аргументов нужно либо переписывать неестественным образом (брать первый или второй фактор?), либо удалять (что невозможно в Java, возможно в C++ только для не виртуальных функций) — проще тогда вообще не наследовать в таком случае, а сделать независимые структуры и все преобразования необходимой иерархии явно писать и проводить.

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

Тогда scale от двух аргументов нужно либо переписывать

Его просто не надо добавлять в исходный класс.

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

А я вот хочу добавить — неуниформное масштабирование кажется в самый раз как метод мутабельному эллипсу, если же делать свободной функцией, то тогда нужны геттеры и сеттеры — setA/setB/getA/getB в интерфейсе круга как-то не очень будет, scale будет масштабировать круг униформно по произведению факторов (фу, или как иначе?). То есть кто-то отнаследует круг который и вовсе не подтип, так же, как разработчики Java/C#, например, сделали массивы ковариантными (не какие-то Васи Пупкины же!), в Scala потом починили и расставили + и - по всем коллекциям где нужно.

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

А я вот хочу добавить

Если твой класс по смыслу предполагает наличие неуниформного масштабирования, то, очевидно, класс который такой возможности не предполагает, чисто логически не может быть его подклассом. Тогда вообще будет логично делать не эллипс, а какой-нибудь nonUnimorfScalingFigure, если требование такого масштабирования именно _существенно_, или вообще выделить в отдельный интерфейс (single responsibility же).

Это, в общем, вопрос проектирования. Естественно, никто не может тебе запретить спроектировать приложение (и иерархию классов, с-но, если речь об ООП) неправильно, так же как никто не может тебе запретить неправильно написать функцию сортировки.

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

Это, в общем, вопрос проектирования.

Да.

Естественно, никто не может тебе запретить спроектировать приложение (и иерархию классов, с-но, если речь об ООП) неправильно, так же как никто не может тебе запретить неправильно написать функцию сортировки.

Вот тут не знаю, стоит ли ставить знак равенства в подобных затыках в декомпозиции и просто очевидно или нет неправильным кодом функций. В том же хаскеле, по ощущениям, основные языковые средства устроены так, что при всём желании найти грабли довольно трудно (в отличии от С++, например, хотя в хаскеле есть свои тараканы уже другого рода, конечно).

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

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

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

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

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

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

вопросы же правильности отображения терминов предметной области на термины языка типами не ловятся

Поэтому есть вариант отображать термины языка на термины предметной области :)

Вот даже пасту нашёл http://www.reddit.com/r/haskell/comments/24adiv/meditations_on_learning_haske...

I routinely write code in Haskell that I am not smart enough to write.

I just break it down into simple enough pieces and make the free theorems strong enough by using sufficiently abstract types that there is only one definition.

I don’t write these stupidly general things because I’m smart. I write them that way so that they write themselves. It is just a case of setting up the questions and asking ‘is this thing an instance of Foo?’ ‘what about that thing?’ and just asking all the questions one after another until all the instances are there.

That is why my code is so meticulous about ensuring all the instances are defined. It is how I convince myself they should or shouldn’t exist. Write them or disprove them one after another.

The plus side of that is that by the time you’re done you’re damn good at going through those motions.

So when i found haskell i slingshotted off through dependent and substructural types. Assuming that if a little was good a lot was better. Made it half way through TaPL and found pure type systems, coq, etc.

I think the power to weight ratio isn’t there. I find that Haskell gives amazingly expressive types that have amazing power for the amount of code you tie up in them and that are very resistant to refactoring.

If i write agda and refactor I scrap and rewrite everything. If i write haskell, and get my tricky logic bits right?

I can refactor it, split things up into classes, play all the squishy software engineering games to get a nice API I want. And in the end if it still compiles I can trust I didn’t screw up the refactoring with a very high degree of assurance.

Really its just that when I wite haskell I write code i can actually for once in my career actually reuse. Not plan to reuse.

Not promise to factor out later. Just outright reuse directly.

90% of my libraries expose all of their guts. No information hiding and there are no problems. Parametricity used right is so nice.

You can test small things in isolation. Free theorems get stronger.

We took the typechecker for $COMPILER and were about half way through writing it in Haskell. This compiler spits out ‘witnesses’ of type checking as ‘core’ expressions for later stages. We had to stop and work on something else for 6 months mid-authorship.

We came back, dusted it off for a week, updating dependencies. Slotted in and got to work…and it worked perfectly the first time. Then we found we could shed 30% of it.

Did so, and it still worked perfectly.

Take 6 month of context away and try to dive into any codebase with non-trivial invariants like a compiler in any other language. That example is just a month or two old even.

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

Поэтому есть вариант отображать термины языка на термины предметной области :)

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

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

все, что не дает ошибок типов, считается «как бы правильным»

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

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