Lex Kravetski (lex_kravetski) wrote,
Lex Kravetski
lex_kravetski

Category:

Чистое лицемерие

Я большой поклонник функций как «граждан первого рода». Это действительно радикально сокращает код и одновременно с тем делает его гораздо более понятным. И заодно более универсальным. По этой причине данный столп функционального программирования сейчас внедрился во все мейнстримные языки, и все, кто его понимает, очень сим довольны.

Однако, стоит вам произнести «функциональная парадигма», как тут же обнаружится какой-то теоретик программирования, слышавший это словосочетание, который обязательно добавит что-то типа: «так несчитово — в настоящем функциональном программировании ещё и все значения неизменяемые». Эти ваши Си-шарпы и Скалы, таким образом, для быдла. В крайнем случае, если вы уж не засели за честный Хаскель, то хотя бы пишите «честно»: никогда не используйте изменяемые значения.

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

В сферическом вакууме кто-то наверно уже написал, например, движок GUI, который одновременно невырвиглазный, удобный в использовании и «чисто функциональный», однако в физической реальности таких нет. И скорее всего не будет — по крайней мере, до тех пор, пока не будут придуманы такие же удачные абстракции, какие придуманы для ссылок на функции.

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

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

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

У вас в GUI есть всякие там поля с параметрами. Если в них что-то поменяется, то из них надо прочитать значение и модифицировать то, что сейчас заселекчено, в соответствии с ними. То есть у вас уже есть запомненные подписки на изменения полей и ещё где-то запомнено то, что именно сейчас заселекчено.

Если заселектится что-то другое, то, наоборот, надо в эти поля вписать то, что соответствует заселекченному. То есть на селект у вас тоже уже есть подписка.

Вы можете попытаться как-то это дело вписать в теоретическую «монаду State», но уверяю, это не только тяжело, но ещё и очень-очень громоздко. Простой и понятный современный движок GUI к такому очень плохо приспособлен, поэтому вам придётся нагородить адский огород.

Ну да ладно, вы, как настоящий самурай, нагородили. But it’s getting worse. Вариантов заселекчивания у вас стопицот, а полей стотыщпицот. Для разных вариантов селектов активизируются разные поля — одни, например, для текста, другие — для картинки, а третьи — для группы фигур. То есть у вас будет не одна «State-монада», а овердофига. И каждую надо вписать в сабжевый огород, который уже и так адский.

Что ещё хуже, сам документ состоит из групп, слоёв, вложенных стилей, тэгов и т.п. То есть объект, который моделирует этот документ, это не три примитивных строки и два числа из рекламного примера, а списки в списках в списках с кучей разнообразных параметров — не только строк и чисел, но и картинок, ссылок на файлы, перечислений элементов группы и так далее.

Вы где-то там на третьем уровне вложенности поменяли стиль абзаца. Будь это «парадигма с человеческим лицом», это изменение выглядело бы как-то типа так:

activeElement.foreach(_.style = Styles.get(currentStyle))



При этом сам activeElement вы до того получили бы после селекшена через GUI при помощи чего-то вроде:

val activeElement =
    activeDocument.paragraphs.find(_.range.contains(selection.position))



Заметьте, как тут всё зашибись, когда с первым столпом, — он избавил вас от отслеживания ошибок: если с позицией в селекшене что-то не так, то автоматически не найдётся активный элемент, а при попытке смены его стиля автоматически ничего не произойдёт. Вам не надо бросать и ловить исключения, проверять мильён условий при каждом действии и т.п. И при этом оно отлично читается. Это ли не счастье?

Ну да, счастье.

Было.

Пока вы не попробовали «чистый столп». Поскольку в нём вы всё ещё можете прочитать что-то «через точку», но уже не можете через неё записать.

Вместо этого вам надо создать копию документа, скопировать в неё все данные, кроме списка параграфов, потом скопировать список параграфов, кроме одного изменившегося, потом скопировать все данные изменившегося параграфа, кроме его стиля, в новый экземпляр параграфа, вписать в копию параграфа новый стиль, добавить новый параграф к новому списку параграфов, а сам новый список вписать в копию документа.

И так надо делать при каждом чихе.

Причём, даже если вы используете что-то типа так называемых «линз», код станет короче, но ни фига не коротким.

Да, в сравнении с огроменной портянкой копирований «в лоб» он, конечно, короткий, но на фоне варианта с точкой — нет.

Там будет что-то типа:

val n =
    activeDocument.paragraphs.find(_.range.contains(selection.position)).index

activeDocument.focus(_.paragraphs).index(n).focus(_.style).replace(Styles.get(currentStyle))



Вам кажется, что не так-то уж и длинно? Тогда представьте, что это ещё в монаде State. Причём не только документ, а селешкн и список стилей тоже. И currentStyle вы модифицируете так же — через «линзу». И selection — через линзу. И вообще всё.

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

Однако если в парадигме здорового человека связь между этими слоями организуется как-то так…

textField1.text <==> myProperty



…то в «чистой» хрен там. Вы уже не можете сделать обёртки значений, которые запоминают ссылку на другую обёртку и при изменении своего значения меняют значение и внутри связанной обёртки тоже. Попытайся вы это повторить, вам нужно было бы при каждом изменении создавать копии обеих обёрток и заново их связывать. Учитывая же, что запоминать копии в переменной тоже как бы нельзя, а можно только в монаде состояния… ну, вы поняли.

Кроме того, у вас уже не два слоя. Нет, эти два тоже никуда не делись — как-то ведь надо разделить графику и модель. Но теперь у вас ещё есть слой линз и слой «чистых данных». Добавив куда-то ещё один элемент, вы, как минимум, должны продублировать оный во всех четырёх слоях. Не исключено, максимально тупо: копипастой его уже существующих соседей по слою.

Нет, конечно, там тоже можно сделать какие-то вспомогательные функции — чтобы хотя бы не копипастить длинные конструкции, однако всё равно вписывание в четыре места неизбежно. В четыре вместо двух.

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

Итак, мы напряглись, наструячили кучу лишнего кода, запутали всё так, что сами через неделю не разберёмся, но, быть может, зато у нас теперь есть «чистый код»? Ну, как это говорится в рекламе: «мы теперь можем спокойно передавать состояние между потоками»?

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

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

object AppState {
 private val _activeDocument: Document = …

 def activeDocument = _activeDocument.deepCopy
}



И всё, ура, любому потоку, который хочет что-то читать, можно даже сам AppState напрямую отправить, не заботясь о синхронизации: он будет брать как бы activeDocument — «через точку», так же как берёт любое другое поле, — но получать не его, а его глубокую копию. Можно даже оптимизировать процесс: при первом запросе создавать неизменяемую копию текущего состояния и запоминать её, пока документ не поменяется, чтобы всем остальным тоже давать её же.

А если поток захочет что-то писать, то синхронизация при любом подходе неизбежна.

И так ведь гораздо, гораздо проще. Какой бонус, блин, мы получили-то, когда попытались поступить иначе? Чувство того, что «у нас тут всё неизменяемое»?

Но даже оно, увы, иллюзия.

Изменяемое всё равно осталось, поскольку мы помним изменения. Да, мы сделали неизменяемые поля, но где хранится текущая копия объекта с неизменяемыми полями? Не в переменной ли?

У нас было, например…

object State {
 var activeDocument: Document
}



И мы при изменении стиля писали…

State.activeDocument.paragraphs(n).style = currentStyle



Теперь мы решили, что надо сделать всё неизменяемым, и написали…

class State {
 val activeDocument: Document
}



Но где-то ведь должен быть экземпляр State, нет? Причём его надо менять при изменении документа или чего-то ещё. То есть где-то написано…

var currentState = new State



…а при каждом изменении делать что-то вроде…

currentState = 
    currentState.focus(_.activeDocument).focus(_.paragraphs)
    .index(n).focus(_.style).replace(currentStyle)



Справа — заметно более длинная строка, копирующая неизменяемый объект с заменой, например, стиля, но слева-то всё равно изменяемое значение.

Да, в рекламных примерах этот момент остаётся за кадром: ведь в них после первого же изменения в документе гипотетические Ворд с Фотошопом записывают документ и закрываются. Но как быть, если нам нужны такие, в которых можно совершить больше одного изменения?

А, ну давайте тогда упакуем это в монаду состояния — это нас типа спасёт.

Однако изменяемой в этом случае будет либо ссылка на монаду, либо какое-то поле внутри этой монады.

Знаете, почему более распространён второй вариант?

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

Вот это надо было бы сделать реальным лозунгом «функциональной чистоты»: не «давайте делать неизменяемые состояния», а «пусть кто-то другой за нас хранит все изменения».

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

Это очень напоминает «чистоту» некоторых религиозных деятелей: они не совершают всяких там грязных деяний, но только потому, что за них их совершает кто-то другой. А вот они зато читают проповеди о «чистоте». Не думают над тем, как сделать так, чтобы грязные деяния не надо было бы совершать никому вообще или хотя бы чтобы необходимых грязных деяний было как можно меньше, а просто аутсорсят их кому-то другому, возможно даже своей деятельностью увеличивая их количество. Но зато не они сами. Не, они-то — «чистые».



doc-файл

Tags: программирование, философия
Subscribe

  • Рандом-преемственность

    Вот как у людей такое вообще в одной голове уживается? Александр Невский, новгородский князь, считающий себя вассалом ордынского хана, в свою…

  • И я тоже

    Реал, не понимаю, что останавливает тех людей, которые «победили, потому что победили наши дедушки». Надо продолжать в том же духе. Дедушку…

  • Мы можем

    Прочитал в рассуждениях любителей «можем перепоказать» ценное дополнение, которое всё проясняет. Хотя что-то там говорят они в основном друг другу, а…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 57 comments

  • Рандом-преемственность

    Вот как у людей такое вообще в одной голове уживается? Александр Невский, новгородский князь, считающий себя вассалом ордынского хана, в свою…

  • И я тоже

    Реал, не понимаю, что останавливает тех людей, которые «победили, потому что победили наши дедушки». Надо продолжать в том же духе. Дедушку…

  • Мы можем

    Прочитал в рассуждениях любителей «можем перепоказать» ценное дополнение, которое всё проясняет. Хотя что-то там говорят они в основном друг другу, а…