Интерфейс vs. протокол.
От: 0x7be СССР  
Дата: 13.04.11 18:07
Оценка: 14 (4)
В современных мэйнстримовых языках интерфейс представляет собой набор функций (сигнатур), которые комонент должен реализовывать. Однако контракт компонента часто подразумевает нечто большее чем просто номенклатура возможных вызовов (ну или сообщений, так тоже можно сказать). В частности, компоненты могут иметь протокол, то есть легальные последовательности вызовов.

Самый простой пример:
interface ISomething
{
  void Open(...);

  void Read(...);
  void Write(...);
 
  void Close(...);
}

Хотя интерфейс позволяет вызывать методы в любом порядке, здесь первым вызовом необходимо делать Open, последним Close, а остальные между ними. К сожалению, протокол тут выражен лишь в комментариях со всеми недостатками такого подхода.
Можно ли выразить протокол в коде с имеющимися средствами языка C#?

Да, есть некоторые варианты. Например, разбить этот интерфейс на два:

interface ISomethingProvider
{
  ISomethingOperator Open(...);
}

interface ISomethingOperator
{
  void Read(...);
  void Write(...);
 
  void Close(...);
}

Имея первый интерфейс программист физически не может вызвать "не те функции" до вызова Open. Впрочем, он может вызвать
Read/Write после Close, что тоже есть нарушение протокола. Более радикальный вариант переработки имеет следующий вариант:
interface ISomethingProvider
{
  ISomethingOperator Open(...);
}

interface ISomethingOperator
{
  ISomethingOperator Read(...);
  ISomethingOperator Write(...);
 
  void Close(...);
}

Причем, у каждого экземпляра интерфейса можно вызвать только один метод один раз, то есть каждая следующая операция должна совершаться с экземпляром интерфейса, возвращенном из предыдущей операции. Операция Close не возвращает интерфейса, следовательно после нее ничего вызвать нельзя.

Фактически это означает, что интерфейсы должны быть линейными типами, то есть допускать лишь одно использование. C# не позволяет наложить такое ограничение на тип, но его можно контролировать в run-time, бросая исключение. Методы, в зависимости от исхода своей работы, могут возвращать разные интерфейсы, пуская пользовательский код по разным путям в графе состояний протокола. Здесь бы очень пригодились алгебраические типы и pattern-matching, которого в C# тоже нет, но кое-что из палок и веревок соорудить тоже можно

В каком-то смысле этот подход — это доведение принципа ISP до крайности, включающей выделение интерфейса не только для отдельного клиента компонента, но и для отдельного протокольного состояния.

В чем преимущество такого подхода? Протокол становится выражен в коде и статически контролируется компилятором. Программист во многом лишается возможности его нарушить из-за небрежности. Что, пожалуй, ещё важнее, изменение такого протокола приведет к поломке собираемости кода, протокол нарушающего, что очень важно при поддержке большого проекта. Можно сказать, что это будет отловлено и юнит-тестами. Но тесты — это, так сказать, экспериментальная проверка на конечном множестве прецедентов, а такое формальное выражение протокола даст возможность через систему типов статически доказать , что протокол соблюден.

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

Предлагаю обсудить эту идею и её жизнеспособность в реальных проектах.
Re: Интерфейс vs. протокол.
От: dimgel Россия https://github.com/dimgel
Дата: 13.04.11 18:22
Оценка:
Здравствуйте, 0x7be, Вы писали:

0>Фактически это означает, что интерфейсы должны быть линейными типами, то есть допускать лишь одно использование. C# не позволяет наложить такое ограничение на тип


А интересно, кто-нибудь может? И как это выглядит? Через систему типов скалы я чёт тоже не могу сообразить. Ссылка на интерфейс должна становиться невалидной после обращения к нему... эт как? Красивая идея, но вот этот момент всё портит.
Re[2]: Интерфейс vs. протокол.
От: 0x7be СССР  
Дата: 13.04.11 18:43
Оценка:
Здравствуйте, dimgel, Вы писали:

D>А интересно, кто-нибудь может? И как это выглядит? Через систему типов скалы я чёт тоже не могу сообразить. Ссылка на интерфейс должна становиться невалидной после обращения к нему... эт как? Красивая идея, но вот этот момент всё портит.

C# этого не позволяет статически проконтролировать в принципе. C# позволяет лишь до некоторой степени автоматизировать выбор исключения при повторном вызове метода. Тут нужна либо прямая поддержка линейных типов в языке, либо некоторый code walker (макрос или внешняя программа, как анализирующая код как FxCop), который будет анализировать AST программы и обнаруживать нарушения.

С другой стороны, этот вопрос может решаться административно. Для всех таких "цепных" интерфейсов будет действовать общее правило — не обращаться по одной ссылке более одного раза. Его выполнение можно контролировать на code review.

Так что методы контроля есть, просто они не столь хороши, как прямая поддержка языка.
Re[2]: Интерфейс vs. протокол.
От: WolfHound  
Дата: 13.04.11 18:47
Оценка: 4 (1)
Здравствуйте, dimgel, Вы писали:

0>>Фактически это означает, что интерфейсы должны быть линейными типами, то есть допускать лишь одно использование. C# не позволяет наложить такое ограничение на тип

D>А интересно, кто-нибудь может? И как это выглядит? Через систему типов скалы я чёт тоже не могу сообразить. Ссылка на интерфейс должна становиться невалидной после обращения к нему... эт как? Красивая идея, но вот этот момент всё портит.
Такие типы называются Uniqueness type.
... << RSDN@Home 1.2.0 alpha 4 rev. 1472>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Re: Интерфейс vs. протокол.
От: WolfHound  
Дата: 13.04.11 18:54
Оценка:
Здравствуйте, 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) А. Эйнштейн
Re[3]: Интерфейс vs. протокол.
От: dimgel Россия https://github.com/dimgel
Дата: 13.04.11 18:59
Оценка:
Здравствуйте, WolfHound, Вы писали:

WH>Такие типы называются Uniqueness type.


По первому абзацу — это вроде другое: там одна ссылка на объект, а тут надо чтобы ссылка стала невалидной после первого вызова метода. Но сначала прочитаю до конца, а ты вот лучше скажи, как такое на Немерле провернуть.
Re[4]: Интерфейс vs. протокол.
От: WolfHound  
Дата: 13.04.11 19:06
Оценка:
Здравствуйте, dimgel, Вы писали:

D>По первому абзацу — это вроде другое: там одна ссылка на объект, а тут надо чтобы ссылка стала невалидной после первого вызова метода.

Оно самое.

D>Но сначала прочитаю до конца, а ты вот лучше скажи, как такое на Немерле провернуть.

Никак. В немерле нет uniqueness типов.
Если бы были то это делалось бы простым макросом.
И систему типов немерле без правки компилятора расширить нельзя.
... << RSDN@Home 1.2.0 alpha 4 rev. 1472>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Re[4]: Интерфейс vs. протокол.
От: dimgel Россия https://github.com/dimgel
Дата: 13.04.11 19:06
Оценка:
Здравствуйте, dimgel, Вы писали:

WH>>Такие типы называются Uniqueness type.


D>По первому абзацу — это вроде другое: там одна ссылка на объект, а тут надо чтобы ссылка стала невалидной после первого вызова метода. Но сначала прочитаю до конца, а ты вот лучше скажи, как такое на Немерле провернуть.


http://stackoverflow.com/questions/5065861/programming-languages-based-on-linear-types

there is a Scala plugin for unique types (it seems it will be "official" part of the language in the upcoming 2.9 release)


Хех, пошёл читать.
Re[5]: Интерфейс vs. протокол.
От: dimgel Россия https://github.com/dimgel
Дата: 13.04.11 19:31
Оценка:
Здравствуйте, WolfHound, Вы писали:

D>>По первому абзацу — это вроде другое: там одна ссылка на объект, а тут надо чтобы ссылка стала невалидной после первого вызова метода.

WH>Оно самое.

Написал вопрос в scala-language: http://groups.google.com/group/scala-language/browse_thread/thread/2a8d00976d9ba482
Продублирую и чуть подправлю в плане сокрытия FileImpl. Я вижу только такой вариант привести одно к другому:

class File { 
    import File.FileImpl

    def open(...) = FileImpl.get(f).open(...) 
    def close() = FileImpl.get(f).close() 
} 

object File {
    private class FileImpl { 
        def open(...): FileImpl 
        def close(): Unit 
    } 

    private object FileImpl { 
         // Every call to any File method passes through here, 
         // so any File instance can receive method call only once. 
        def get(f: File @Unique): FileImpl 
    } 
}
Re: Интерфейс vs. протокол.
От: Sorc17 Россия  
Дата: 13.04.11 21:10
Оценка: +1 -2
Я понимаю о чём вы говорите, очень интересная тема. Но пример у вас совсем не показательный ...

... в read()/write() обычно передаётся дескриптор, который создаётся с помощью open(), поэтому вызвать их перед вызовом open() значит передать им не верный дескриптор, а то что дескриптор может быть не верным это нормально: он может испортится и между вызовами read() и write(), так что они "обязаны" прореагировать не неправильный дескриптор. То есть неправильно вызвать read()/write() это значит вообще не понимать данного интерфейса, ни зачем он, ни что делает, ни даже определение функций посмотреть. Программу же не слепоглухонемой пишет, да? close() вы никак не заставите вызвать программиста, потому что нет заранее известного момента когда его нужно вызывать: может быть после последнего read()/write(), а может быть при завершении работы программы, а может быть никогда (операционная система сама закроет все дескрипторы при завершении программы).

Как вариант можно спрятать open() и close() внутрь read() и write(). Например как в функциях PHP: file_get_contents() и file_put_contents().

Полчаса лазал по http://download.oracle.com/javase/6/docs/api/overview-tree.html в поисках наиболее наглядного примера, но устал и бросил, простите.
Для нас [Thompson, Rob Pike, Robert Griesemer] это было просто исследование. Мы собрались вместе и решили, что ненавидим C++ [смех].
Re[2]: Интерфейс vs. протокол.
От: 0x7be СССР  
Дата: 14.04.11 04:14
Оценка:
Здравствуйте, WolfHound, Вы писали:

WH>Здравствуйте, 0x7be, Вы писали:


0>>Предлагаю обсудить эту идею и её жизнеспособность в реальных проектах.

WH>В сингулярити это сделали. Правда поддержку компилятороа до ума не довели.
Занятно. А что в компиляторе недоведено до ума?
Re[2]: Интерфейс vs. протокол.
От: 0x7be СССР  
Дата: 14.04.11 04:52
Оценка:
Здравствуйте, Sorc17, Вы писали:

S>Я понимаю о чём вы говорите, очень интересная тема. Но пример у вас совсем не показательный ...

Пример мог бы быть лучше, не спорю.

S>... То есть неправильно вызвать read()/write() это значит вообще не понимать данного интерфейса, ни зачем он, ни что делает, ни даже определение функций посмотреть. Программу же не слепоглухонемой пишет, да?

Понятно, что программист будет разбираться в интерфейсе. Вопрос в другом, как ему помочь разобраться, как помочь ему не нарушить протокол интерфейса и, что на мой взгляд важнее, как помочь программисту безопасно изменять протокол интерфейса при дальнейшей поддержке. Мое поредложение позволяет часть протокола выразить формально через систему типов языка, что позволит использовать компилятор для проверки корректности.
Re[3]: Интерфейс vs. протокол.
От: Sorc17 Россия  
Дата: 14.04.11 05:40
Оценка:
Здравствуйте, 0x7be, Вы писали:

0>Мое предложение позволяет часть протокола выразить формально через систему типов языка, что позволит использовать компилятор для проверки корректности.


Можно как-то при этом ещё оставить программисту возможность вызывать методы "не правильно"? Например я могу реализовать open(), read() и write() пустыми, потому что моя реализация интерфейса нужна только для того чтобы закрывать полученные откуда-то свыше дескрипторы вызывая close().
Для нас [Thompson, Rob Pike, Robert Griesemer] это было просто исследование. Мы собрались вместе и решили, что ненавидим C++ [смех].
Re[4]: Интерфейс vs. протокол.
От: 0x7be СССР  
Дата: 14.04.11 05:56
Оценка: +1
Здравствуйте, Sorc17, Вы писали:

S>Можно как-то при этом ещё оставить программисту возможность вызывать методы "не правильно"?

Зачем?
Re: Интерфейс vs. протокол.
От: Sinix  
Дата: 14.04.11 06:17
Оценка: 1 (1)
Здравствуйте, 0x7be, Вы писали:

0>В современных мэйнстримовых языках интерфейс представляет собой набор функций (сигнатур), которые комонент должен реализовывать. Однако контракт компонента часто подразумевает нечто большее чем просто номенклатура возможных вызовов (ну или сообщений, так тоже можно сказать). В частности, компоненты могут иметь протокол, то есть легальные последовательности вызовов.

...

Угу. Идея абсолютно верная.

Я вам не скажу за всю Одессу, но МС активно заигрывает с верифицируемым кодом. Всё идёт к тому, что это станет основной фичей или 6го, или 7го шарпа

Про singularity уже писали, в ту же степь идёт Midori (но там ничего интересного нет) и Verve (а вот про неё почитать стоит).

Если смотреть в сторону мейнстрима, то тут у МС Axum в инкубаторе и Code Contracts в стадии тестирования на мышах.

Осталось прикрутить сахар для continuation passing (хотя оно уже в принципе есть в виде await + RX) — и у нас получается забавный аналог эрланга
Re[3]: Интерфейс vs. протокол.
От: WolfHound  
Дата: 14.04.11 06:53
Оценка:
Здравствуйте, 0x7be, Вы писали:

0>Занятно. А что в компиляторе недоведено до ума?

Нет поддержки uniqueness type.
... << RSDN@Home 1.2.0 alpha 4 rev. 1472>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Re[5]: Интерфейс vs. протокол.
От: Sorc17 Россия  
Дата: 14.04.11 07:11
Оценка:
Здравствуйте, 0x7be, Вы писали:

S>>Можно как-то при этом ещё оставить программисту возможность вызывать методы "не правильно"?

0>Зачем?

Я уже написал пример. Если вы сделаете так, что close() из "реализации интерфейса" вызвать невозможно не вызвав сначала open() да ещё и close() можно вызвать только для того, что возвращает именно тот самый вызов open(), то вы сильно ограничите применение вашего "интерфейса" как инструмента, я считаю. Если бы я хотел сделать так чтобы вызов read(), write() и close() нельзя было вызывать без open(), то я бы вовсе не выносил в интерфейс open() и close(). По-моему это логично, нет? Зачем они в интерфейсе?
Для нас [Thompson, Rob Pike, Robert Griesemer] это было просто исследование. Мы собрались вместе и решили, что ненавидим C++ [смех].
Re: Интерфейс vs. протокол.
От: D. Mon Великобритания http://thedeemon.livejournal.com
Дата: 14.04.11 09:04
Оценка: 6 (1)
Здравствуйте, 0x7be, Вы писали:

0>Фактически это означает, что интерфейсы должны быть линейными типами, то есть допускать лишь одно использование. C# не позволяет наложить такое ограничение на тип, но его можно контролировать в run-time, бросая исключение. Методы, в зависимости от исхода своей работы, могут возвращать разные интерфейсы, пуская пользовательский код по разным путям в графе состояний протокола. Здесь бы очень пригодились алгебраические типы и pattern-matching, которого в C# тоже нет, но кое-что из палок и веревок соорудить тоже можно


Имхо, ближе всего к описанному то, как подобные штуки (например, работа с консолью или файлами) сделаны в языке Clean. Там как раз на уникальных типах это все.
Re: Интерфейс vs. протокол.
От: Undying Россия  
Дата: 14.04.11 10:19
Оценка: +1
Здравствуйте, 0x7be, Вы писали:

0>
0>interface ISomethingProvider
0>{
0>  ISomethingOperator Open(...);
0>}

0>interface ISomethingOperator
0>{
0>  ISomethingOperator Read(...);
0>  ISomethingOperator Write(...);
0>  void Close(...);
0>}
0>

0>Причем, у каждого экземпляра интерфейса можно вызвать только один метод один раз, то есть каждая следующая операция должна совершаться с экземпляром интерфейса, возвращенном из предыдущей операции. Операция Close не возвращает интерфейса, следовательно после нее ничего вызвать нельзя.

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

От проблемы с вызовом Open можно легко избавиться и так, делая его в конструкторе. Проблему с невызовом Close или вызовом его в не тот момент это код никак не помогает решить. От Close в принципе тоже можно избавиться, заменив на Finalize.

Так бы я сказал, что проблемы с последовательностью вызовов методов это обычно архитектурная проблема, и, как правило, задачу можно решить так, что таких проблем не возникает.
Re[2]: Интерфейс vs. протокол.
От: dotneter  
Дата: 14.04.11 11:07
Оценка:
Здравствуйте, Undying, Вы писали:


U>Так бы я сказал, что проблемы с последовательностью вызовов методов это обычно архитектурная проблема, и, как правило, задачу можно решить так, что таких проблем не возникает.

Задача называется статическая проверка состояния конечного автомата, как вы ее будете решать архитектурно?
... << RSDN@Home 1.2.0 alpha 4 rev. 1111>>
Talk is cheap. Show me the code.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.