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

Честно говоря, я бы хотел начать со стороны поистине высокоуровневых абстракций, но мой предыдущий опыт показывает, что даже простейшие математические истины на нематематическом языке «не звучат». Лишь попутно замечу, что все то, о чем я говорю, рождено математической мыслью — и, соответственно, может быть формализовано строго.
А теперь — конкретика. Наверно, все более-менее «шарящие» потрудились хотя бы однажды прочесть эпохальный труд GoF; наверно, даже «нешарящие» знают, что есть такой паттерн — iterator. И уж точно все им пользуются каждый день. Итак (держась условного C#):

bool MoveNext();
T Current { get; }

… а теперь то же самое, но в без синтаксических шумов (показательно, что по факту «без синтаксических шумов» означает на Haskell'е):

MoveNext :: () -> Bool
Current :: () -> t

… забывая на время о типах и сосредотачиваясь на самой стрелке (->), невольно приходишь к вопросу о том, а что будет, если ее «обратить вспять»? Тогда получится:

MoveNext' :: Bool -> ()
Current' :: t -> ()

Легкое ощущение дисгармонии, да? Если с t -> () никаких проблем, то Bool -> () выглядит более, чем подозрительно. И действительно — этот флаг использовался, чтобы сигналить об окончании последовательности, так? В перевернутом виде, случай True -> () не имеет никакого полезного смысла — достаточно только False -> (). Потерялась бинарная неопределенность, которую обеспечивает тип Bool — и потому оно должно выглядеть так:

completed :: () -> ()
next :: t -> ()

Ничего не напоминает? Как насчет reactive extensions?
Воу! Сигнатура (ну, точнее, сигнатуры, но сам подход-то работает именно на индивидуальном уровне) функции одного (почти) классного (мини)фреймворка поведала нам о другом не менее крутом, да? Нужно только быть внимательным и слышать.

Благая весть миру от теории категорий
Кто в теме, тот, конечно, знает, что пример с IEnumerator <-> IObserver не нов.
Удивляет, что редко кто осведомлен о том, что это далеко не единственный пример (правда, надо отдать должное, конкретно этот — очень яркий).

Любопытные могут взять оставшиеся паттерны GoF и посмотреть на их «дуалов». Нередко будут получаться полнейшие бессмыслицы — это нормально: сама по себе «дуальность» выше земного мира и не считает себя обязанной считаться с практической пользой: но ведь и Солнцу, чтобы светить, от людей ничего не надо.

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

Лучше подведу итоги, целиком и полностью основанные на моем опыте:

  1. Сигнатуры функций умеют разговаривать, но программисты часто остаются глухи.
  2. Каждый раз, когда ты встречаешь хорошо спроектированную функцию, спроси себя о том, каков ее дуал и мог ли он быть полезен для каких-либо задач.
  3. Если дуал существует, но требует «легкой модификации» как в примере выше, то скорее всего исходная сигнатура обладает некоторыми неявными дефектами; может быть есть смысл ее переопределить так, чтобы такой проблемы не было?

Комментарии (2)


  1. samsergey
    26.01.2019 06:04

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


    1. f2heartz Автор
      26.01.2019 07:06

      Насчет семантики Хаскеля — бесспорно. Но хотелось просто удержать внимание читателя на сигнатурах, пренебрегая по ходу некоторыми вещами типа IO. Сначала думал держаться чисто математической нотации, но Хаскель по сути так и делает.

      Спасибо за отзыв!