В современных мэйнстримовых языках интерфейс представляет собой набор функций (сигнатур), которые комонент должен реализовывать. Однако контракт компонента часто подразумевает нечто большее чем просто номенклатура возможных вызовов (ну или сообщений, так тоже можно сказать). В частности, компоненты могут иметь протокол, то есть легальные последовательности вызовов.
Хотя интерфейс позволяет вызывать методы в любом порядке, здесь первым вызовом необходимо делать Open, последним Close, а остальные между ними. К сожалению, протокол тут выражен лишь в комментариях со всеми недостатками такого подхода.
Можно ли выразить протокол в коде с имеющимися средствами языка C#?
Да, есть некоторые варианты. Например, разбить этот интерфейс на два:
Имея первый интерфейс программист физически не может вызвать "не те функции" до вызова Open. Впрочем, он может вызвать
Read/Write после Close, что тоже есть нарушение протокола. Более радикальный вариант переработки имеет следующий вариант:
Причем, у каждого экземпляра интерфейса можно вызвать только один метод один раз, то есть каждая следующая операция должна совершаться с экземпляром интерфейса, возвращенном из предыдущей операции. Операция Close не возвращает интерфейса, следовательно после нее ничего вызвать нельзя.
Фактически это означает, что интерфейсы должны быть линейными типами, то есть допускать лишь одно использование. C# не позволяет наложить такое ограничение на тип, но его можно контролировать в run-time, бросая исключение. Методы, в зависимости от исхода своей работы, могут возвращать разные интерфейсы, пуская пользовательский код по разным путям в графе состояний протокола. Здесь бы очень пригодились алгебраические типы и pattern-matching, которого в C# тоже нет, но кое-что из палок и веревок соорудить тоже можно
В каком-то смысле этот подход — это доведение принципа ISP до крайности, включающей выделение интерфейса не только для отдельного клиента компонента, но и для отдельного протокольного состояния.
В чем преимущество такого подхода? Протокол становится выражен в коде и статически контролируется компилятором. Программист во многом лишается возможности его нарушить из-за небрежности. Что, пожалуй, ещё важнее, изменение такого протокола приведет к поломке собираемости кода, протокол нарушающего, что очень важно при поддержке большого проекта. Можно сказать, что это будет отловлено и юнит-тестами. Но тесты — это, так сказать, экспериментальная проверка на конечном множестве прецедентов, а такое формальное выражение протокола даст возможность через систему типов статически доказать , что протокол соблюден.
Недостаток подхода очевиден: усложнение интерфейса компонента, растаскивание его функциональности по множеству интерфейсов, усложнение реализации, возможно некоторый overhead во время исполнения, усложнение пользовательского кода.
Предлагаю обсудить эту идею и её жизнеспособность в реальных проектах.
Здравствуйте, 0x7be, Вы писали:
0>Фактически это означает, что интерфейсы должны быть линейными типами, то есть допускать лишь одно использование. C# не позволяет наложить такое ограничение на тип
А интересно, кто-нибудь может? И как это выглядит? Через систему типов скалы я чёт тоже не могу сообразить. Ссылка на интерфейс должна становиться невалидной после обращения к нему... эт как? Красивая идея, но вот этот момент всё портит.
Здравствуйте, dimgel, Вы писали:
D>А интересно, кто-нибудь может? И как это выглядит? Через систему типов скалы я чёт тоже не могу сообразить. Ссылка на интерфейс должна становиться невалидной после обращения к нему... эт как? Красивая идея, но вот этот момент всё портит.
C# этого не позволяет статически проконтролировать в принципе. C# позволяет лишь до некоторой степени автоматизировать выбор исключения при повторном вызове метода. Тут нужна либо прямая поддержка линейных типов в языке, либо некоторый code walker (макрос или внешняя программа, как анализирующая код как FxCop), который будет анализировать AST программы и обнаруживать нарушения.
С другой стороны, этот вопрос может решаться административно. Для всех таких "цепных" интерфейсов будет действовать общее правило — не обращаться по одной ссылке более одного раза. Его выполнение можно контролировать на code review.
Так что методы контроля есть, просто они не столь хороши, как прямая поддержка языка.
Здравствуйте, dimgel, Вы писали:
0>>Фактически это означает, что интерфейсы должны быть линейными типами, то есть допускать лишь одно использование. C# не позволяет наложить такое ограничение на тип D>А интересно, кто-нибудь может? И как это выглядит? Через систему типов скалы я чёт тоже не могу сообразить. Ссылка на интерфейс должна становиться невалидной после обращения к нему... эт как? Красивая идея, но вот этот момент всё портит.
Такие типы называются Uniqueness type.
... << RSDN@Home 1.2.0 alpha 4 rev. 1472>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Здравствуйте, 0x7be, Вы писали:
0>Предлагаю обсудить эту идею и её жизнеспособность в реальных проектах.
В сингулярити это сделали. Правда поддержку компилятороа до ума не довели.
public contract TcpConnectionContract {
in message Connect(uint dstIP, ushort dstPort);
out message Ready();
// Initial state
state Start : Ready! -> ReadyState;
state ReadyState : one {
Connect? -> ConnectResult;
BindLocalEndPoint? -> BindResult;
Close? -> Closed;
}
state BindResult : one {
OK! -> Bound;
InvalidEndPoint! -> ReadyState;
}
in message Listen();
state Bound : one {
Listen? -> ListenResult;
Connect? -> ConnectResult;
Close? -> Closed;
} ...
... << RSDN@Home 1.2.0 alpha 4 rev. 1472>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Здравствуйте, WolfHound, Вы писали:
WH>Такие типы называются Uniqueness type.
По первому абзацу — это вроде другое: там одна ссылка на объект, а тут надо чтобы ссылка стала невалидной после первого вызова метода. Но сначала прочитаю до конца, а ты вот лучше скажи, как такое на Немерле провернуть.
Здравствуйте, dimgel, Вы писали:
D>По первому абзацу — это вроде другое: там одна ссылка на объект, а тут надо чтобы ссылка стала невалидной после первого вызова метода.
Оно самое.
D>Но сначала прочитаю до конца, а ты вот лучше скажи, как такое на Немерле провернуть.
Никак. В немерле нет uniqueness типов.
Если бы были то это делалось бы простым макросом.
И систему типов немерле без правки компилятора расширить нельзя.
... << RSDN@Home 1.2.0 alpha 4 rev. 1472>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Здравствуйте, dimgel, Вы писали:
WH>>Такие типы называются Uniqueness type.
D>По первому абзацу — это вроде другое: там одна ссылка на объект, а тут надо чтобы ссылка стала невалидной после первого вызова метода. Но сначала прочитаю до конца, а ты вот лучше скажи, как такое на Немерле провернуть.
Здравствуйте, WolfHound, Вы писали:
D>>По первому абзацу — это вроде другое: там одна ссылка на объект, а тут надо чтобы ссылка стала невалидной после первого вызова метода. WH>Оно самое.
Я понимаю о чём вы говорите, очень интересная тема. Но пример у вас совсем не показательный ...
... в read()/write() обычно передаётся дескриптор, который создаётся с помощью open(), поэтому вызвать их перед вызовом open() значит передать им не верный дескриптор, а то что дескриптор может быть не верным это нормально: он может испортится и между вызовами read() и write(), так что они "обязаны" прореагировать не неправильный дескриптор. То есть неправильно вызвать read()/write() это значит вообще не понимать данного интерфейса, ни зачем он, ни что делает, ни даже определение функций посмотреть. Программу же не слепоглухонемой пишет, да? close() вы никак не заставите вызвать программиста, потому что нет заранее известного момента когда его нужно вызывать: может быть после последнего read()/write(), а может быть при завершении работы программы, а может быть никогда (операционная система сама закроет все дескрипторы при завершении программы).
Как вариант можно спрятать open() и close() внутрь read() и write(). Например как в функциях PHP: file_get_contents() и file_put_contents().
Здравствуйте, WolfHound, Вы писали:
WH>Здравствуйте, 0x7be, Вы писали:
0>>Предлагаю обсудить эту идею и её жизнеспособность в реальных проектах. WH>В сингулярити это сделали. Правда поддержку компилятороа до ума не довели.
Занятно. А что в компиляторе недоведено до ума?
Здравствуйте, Sorc17, Вы писали:
S>Я понимаю о чём вы говорите, очень интересная тема. Но пример у вас совсем не показательный ...
Пример мог бы быть лучше, не спорю.
S>... То есть неправильно вызвать read()/write() это значит вообще не понимать данного интерфейса, ни зачем он, ни что делает, ни даже определение функций посмотреть. Программу же не слепоглухонемой пишет, да?
Понятно, что программист будет разбираться в интерфейсе. Вопрос в другом, как ему помочь разобраться, как помочь ему не нарушить протокол интерфейса и, что на мой взгляд важнее, как помочь программисту безопасно изменять протокол интерфейса при дальнейшей поддержке. Мое поредложение позволяет часть протокола выразить формально через систему типов языка, что позволит использовать компилятор для проверки корректности.
Здравствуйте, 0x7be, Вы писали:
0>Мое предложение позволяет часть протокола выразить формально через систему типов языка, что позволит использовать компилятор для проверки корректности.
Можно как-то при этом ещё оставить программисту возможность вызывать методы "не правильно"? Например я могу реализовать open(), read() и write() пустыми, потому что моя реализация интерфейса нужна только для того чтобы закрывать полученные откуда-то свыше дескрипторы вызывая close().
Для нас [Thompson, Rob Pike, Robert Griesemer] это было просто исследование. Мы собрались вместе и решили, что ненавидим C++ [смех].
Здравствуйте, 0x7be, Вы писали:
0>В современных мэйнстримовых языках интерфейс представляет собой набор функций (сигнатур), которые комонент должен реализовывать. Однако контракт компонента часто подразумевает нечто большее чем просто номенклатура возможных вызовов (ну или сообщений, так тоже можно сказать). В частности, компоненты могут иметь протокол, то есть легальные последовательности вызовов.
...
Угу. Идея абсолютно верная.
Я вам не скажу за всю Одессу, но МС активно заигрывает с верифицируемым кодом. Всё идёт к тому, что это станет основной фичей или 6го, или 7го шарпа
Про singularity уже писали, в ту же степь идёт Midori (но там ничего интересного нет) и Verve (а вот про неё почитать стоит).
Если смотреть в сторону мейнстрима, то тут у МС Axum в инкубаторе и Code Contracts в стадии тестирования на мышах.
Осталось прикрутить сахар для continuation passing (хотя оно уже в принципе есть в виде await + RX) — и у нас получается забавный аналог эрланга
Здравствуйте, 0x7be, Вы писали:
S>>Можно как-то при этом ещё оставить программисту возможность вызывать методы "не правильно"? 0>Зачем?
Я уже написал пример. Если вы сделаете так, что close() из "реализации интерфейса" вызвать невозможно не вызвав сначала open() да ещё и close() можно вызвать только для того, что возвращает именно тот самый вызов open(), то вы сильно ограничите применение вашего "интерфейса" как инструмента, я считаю. Если бы я хотел сделать так чтобы вызов read(), write() и close() нельзя было вызывать без open(), то я бы вовсе не выносил в интерфейс open() и close(). По-моему это логично, нет? Зачем они в интерфейсе?
Для нас [Thompson, Rob Pike, Robert Griesemer] это было просто исследование. Мы собрались вместе и решили, что ненавидим C++ [смех].
Здравствуйте, 0x7be, Вы писали:
0>Фактически это означает, что интерфейсы должны быть линейными типами, то есть допускать лишь одно использование. C# не позволяет наложить такое ограничение на тип, но его можно контролировать в run-time, бросая исключение. Методы, в зависимости от исхода своей работы, могут возвращать разные интерфейсы, пуская пользовательский код по разным путям в графе состояний протокола. Здесь бы очень пригодились алгебраические типы и pattern-matching, которого в C# тоже нет, но кое-что из палок и веревок соорудить тоже можно
Имхо, ближе всего к описанному то, как подобные штуки (например, работа с консолью или файлами) сделаны в языке Clean. Там как раз на уникальных типах это все.
0>Причем, у каждого экземпляра интерфейса можно вызвать только один метод один раз, то есть каждая следующая операция должна совершаться с экземпляром интерфейса, возвращенном из предыдущей операции. Операция Close не возвращает интерфейса, следовательно после нее ничего вызвать нельзя.
Непонятно в чем заключается выигрыш, при том что огромный проигрыш очевиден.
От проблемы с вызовом Open можно легко избавиться и так, делая его в конструкторе. Проблему с невызовом Close или вызовом его в не тот момент это код никак не помогает решить. От Close в принципе тоже можно избавиться, заменив на Finalize.
Так бы я сказал, что проблемы с последовательностью вызовов методов это обычно архитектурная проблема, и, как правило, задачу можно решить так, что таких проблем не возникает.
U>Так бы я сказал, что проблемы с последовательностью вызовов методов это обычно архитектурная проблема, и, как правило, задачу можно решить так, что таких проблем не возникает.
Задача называется статическая проверка состояния конечного автомата, как вы ее будете решать архитектурно?