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

Цензура ради качественной музыки



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

Возьмём для примера классическую песню Коула Портера 1928 года Let’s Do It (Let’s Fall in Love). Все мы понимаем, что подразумевается под «It» и это определённо не «давай влюбимся». Подозреваю, что автору пришлось добавить часть в скобках, чтобы избежать цензуры.

Перенесёмся в 2011 год и посмотрим на Slob on my Knob группы Three 6 Mafia. За исключением первого метафорического куплета всё остальное до отвращения очевидно.

Если отвлечься на минуту от художественности исполнения (или его отсутствия), то можно сказать, что в песне Коула Портера намёками говорится о том, что Three 6 Mafia вываливает на нас с невыносимыми подробностями, не оставляющими ничего для работы воображения.

Проблема заключается в том, что если не разделяете взглядов на занятия сексом, описываемых в текстах Three 6 Mafia, то посчитаете песню в лучшем случае вульгарной и совершенно не раскрывающей тему. А включив песню Коула Портера, слушатель может вызвать в воображении собственную фантазию.

То есть ограничения могут делать предмет более притягательным.

Акула сломалась



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

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

Почему этот фильм намного лучше того, в котором показывают акулу? Потому что каждый зритель самостоятельно заполняет пробелы с помощью своего воображения. Он вспоминает собственные фобии и проецирует их на экран. Поэтому страх ПЕРСОНАЛЕН для каждого зрителя.

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

Почти все люди считают, что они видели то, как застрелили маму Бэмби. Но мы не только не видим, как в неё стреляют — мы даже никогда не видели её ПОСЛЕ выстрела. Но люди могут поклясться, что они видели обе сцены. Но этого НИКОГДА не показывали.

Итак, ограничения делают всё лучше. Гораздо лучше.

Возможности выбора повсюду



Представьте, что вы художник, и я прошу вас написать картину. Единственное, чего я прошу: «Нарисуйте мне что-нибудь красивое. То, что мне понравится».

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

Потому что вариантов слишком много. Вы можете нарисовать буквально что угодно. Я не поставил перед вами НИКАКИХ ограничений. Это явление называется парадоксом выбора.

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

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

И пока неосознанно вы смогли бы начать писать морской пейзаж.

Итак, ограничения делают творчество проще.

Аппаратное обеспечение проще, чем программное



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

Графическая карта обладает собственными резисторами, которыми управляет только она. Инженеры аппаратного обеспечения делают так не потому, что они хотят продать больше резисторов. Они делают так, потому что у них нет выбора.

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

Такие ограничения делают работу с аппаратным обеспечением проще, чем работа с ПО.

В программах нет ничего невозможного



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

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

Обычно операционная система ограничивает действия, которые программа может выполнить за своими пределами. Но никакие ограничения не накладываются на то, что она может делать с принадлежащими ей кодом и данными.

Именно отсутствие ограничений делает написание и поддержку ПО таким сложным делом.

Как правильно ставить ограничения в разработке ПО



Мы знаем, что при разработке ПО нужны ограничения, и по опыту знаем, что ограничения в других творческих профессиях могут пойти нам во благо.

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

Мы должны ограничивать себя сами. Но мы должны гарантировать, что эти ограничения пойдут всем во благо. Так какие же границы мы должны выбрать и как нам вообще принимать такие решения?

Чтобы ответить на этот вопрос, мы можем положиться на наш опыт и годы практики. Но самым полезным инструментом являются наши прошлые ошибки.

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

Let my People Go



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

Индустрия поняла, что такая практика контрпродуктивна и сначала запретила использование в коде конструкции GOTO тех языков, в которых она разрешалась.

Со временем, новые языки программирования полностью отказались от поддержки GOTO. Они стали называться языками структурного программирования. И сегодня все популярные высокоуровневые языки не содержат GOTO.

Когда это произошло, некоторые стали жаловаться, что новые языки слишком строги и что при использовании GOTO писать код проще.

Но победили более прогрессивно мыслящие, и мы должны быть благодарны им за отказ от такого разрушительного инструмента.

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

Компьютеры по-прежнему могут выполнять GOTO. На самом деле, им это даже необходимо. Просто мы, как индустрия в целом, решила ограничить непосредственное использование их программистами. Все компьютерные языки компилируются в код, использующий GOTO. Но разработчики языков создали конструкции, использующие более упорядоченное ветвление, например, с помощью конструкции break, выполняющей выход из цикла for.

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

Надеваем кандалы



Так что же является GOTO сегодня и что разработчики языков готовят для нас, ничего не подозревающих программистов?

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

  1. Сложность
  2. Многократное использование
  3. Глобальное изменяемое состояние
  4. Динамическая типизация
  5. Тестирование
  6. Крах Закона Мура

Как мы можем ограничить возможности программистов так, чтобы решить эти проблемы?

Сложность



Сложность растёт со временем. То, что изначально является простой системой, со временем эволюционирует в сложную. То, что начинается как сложная система, со временем эволюционирует в хаос.

Так как же нам ограничить программистов, чтобы помочь им снизить сложность?

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

Многие функциональные языки программирования, особенно самые чистые, реализуют оба эти эффекта.

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

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

Чистые функции работают только с переданными им данными, вычисляют свои результаты и передают их. Каждый раз, когда вы вызываете чистую функцию с одинаковыми входными данными, она ВСЕГДА будет выдавать одинаковые выходные данные.

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

Ограничивая программистов исключительно чистыми функциями, мы значительно ограничиваем сложность, потому что функции могут иметь только локальное влияние; кроме того, это помогает разработчикам естественным образом разбивать на части их программы.

Многократное использование



Индустрия программного обеспечения борется с этой проблемой почти с самого момента появления программирования. Сначала у нас были библиотеки, потом структурное программирование, а потом — объектно-ориентированное наследование.

Все эти подходы имеют ограниченную привлекательность и успех. Но есть один способ, который всегда работает и применялся почти каждым программистом — Copy/Paste, или «копипаста».

Если вы копируете и вставляете свой код, то делаете что-то не так.

Мы не можем запретить программистам копипастить, потому что они всё-таки пишут программы в виде текста, однако мы можем дать им кое-что получше.

В функциональном программировании есть стандартные практики, которые намного лучше копипасты, а именно функции высшего порядка, каррирование (карринг) и композиция.

Функции высшего порядка позволяют программистам передавать параметры, которые являются данными и функциями. В языках, не поддерживающих эту особенность, единственным решением является копирование и вставка функции с последующим редактированием логики. Благодаря функциям высшего порядка логику можно передавать как параметр в виде функции.

Каррирование (карринг) позволяет применять к функции по одному параметру за раз. Это позволяет программистам писать генерализированные версии функций и «запекать» некоторые из параметров для создания более специализированных версий.

Композиция позволяет программистам собирать функции как кубики Lego, позволяя им повторно использовать функционал, который они или другие встроили в конвейер, в котором данные переходят от одной функции к другой. Упрощённой формой этого являются конвейеры Unix.

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

Глобальное изменяемое состояние



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

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

Где-то в программе недопустимым образом изменяется состояние. Такие «баги» обычно одни из самым сложных в исправлении. Почему? Потому что их очень сложно воспроизвести.

Если вам не удаётся стабильно воспроизвести такой «баг», то вы не сможете найти способ его устранения. Вы можете проверить своё исправление и ничего не произойдёт. Но получилось ли так, потому что проблема устранена, или потому, что она пока не возникла?

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

Функциональное программирование решает эту проблему, устанавливая ограничения для программистов на уровне языка. Программисты не могут создавать изменяемые переменные.

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

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

Учтите, что такие изменения происходят «под капотом». Как и в былые времена уничтожения GOTO, компилятор и выполняемая программа по-прежнему применяют GOTO. Они просто недоступны программистам.

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

И когда в 98% кода отсутствуют побочные эффекты, портящие состояние баги могут оставаться только в оставшихся 2%. Это даёт программисту хороший шанс найти ошибки такого типа, потому что опасные части загнаны в загон.

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

Динамическая типизация



Есть ещё одна долгий и старый спор о статической типизации и динамической типизации. Статическая типизация — это когда тип переменной проверяется на этапе компиляции. После того, как вы зададите тип, компилятор помогает определить, используете ли вы его правильно.

Возражения против статической типизации заключаются в том, что она навешивает на программиста ненужную ношу и загрязняет код подробной информацией о типизации. И эта информация о типизации синтаксически «шумная», потому что находится рядом с определением функций.

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

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

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

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

Это значит, что мы можем пользоваться статичной типизацией без излишнего задания типов. Рекомендации говорят нам, что типизация должна задаваться, а не определяться компилятором, но в таких языках, как Haskell и Elm, синтаксис типизации на самом деле не разрушает структуру и довольно полезен.

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

По сравнению с ними, системы типов Haskell и Elm на самом деле помогают программистам кодировать лучше и уведомляют их на этапе компиляции, если программа не будет работать правильно.

Итак, ограничивая программистов хорошей статической типизацией, компилятор может помочь в распознавании ошибок, определять типы и помогать в кодировании, а не отягощать разработчика многословной, навязчивой информацией о типах.

Тестирование



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

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

  1. Не писать тесты
  2. Имитировать базу данных или сервер

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

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

Тестирование чистых функций намного проще. Но нам всё равно нужно писать код тестов, отравляющий нам жизнь. Или всё же нет?

Оказывается, что существуют программы для автоматического тестирования функциональных программ. Единственное, что должен предоставить программист — свойства, которых должны придерживаться функции, например, обратные функции. Автоматизированный тестер Haskell называется QuickCheck.

Итак, ограничивая большинство функций так, чтобы они были чистыми, мы делаем тестирование намного проще, а в некоторых случаях просто тривиальным.

Крах Закона Мура



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

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

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

Удвоение вычислительной мощности означает удвоение потребляемого процессором питания. Это приведёт к ещё большему расходу батарей, чем сегодня. Аккумуляторные технологии сильно отстают от неутолимых аппетитов пользователей.

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

Сегодня реализация параллелизма — тяжкая ноша для программиста. Программу необходимо разрезать вдоль и поперёк на параллельные части. Это непростая задача. И на практике, в таких языках, как JavaScript, программисты не могут управлять этим, потому что код не может выполняться параллельно, он однопоточный.

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

Ограничиваясь только чистыми функциями, программист избавляется от заботы о параллелизме.

Функциональные программы смогут оптимальнее использовать преимущества многоядерных машин, не увеличивая сложность для разработчика.

Делать больше благодаря меньшим возможностям



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

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

Мне кажется, что и для нас, разработчиков программного обеспечения, настало время ограничивать себя, чтобы достичь большего.

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


  1. Sinatr
    05.01.2018 11:35
    +5

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


    1. DEmon_CDXLIV
      05.01.2018 12:06
      +1

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


    1. System12
      05.01.2018 13:04
      +2

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


  1. SirEdvin
    05.01.2018 11:44
    +3

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

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

    Или из более простых вариантов, систему алертов для мониторинга, на подобии alertmanager, где глобальное состояние, это единственное, что в этой программе вообще есть.


    1. DEmon_CDXLIV
      05.01.2018 11:59
      +2

      Вы упустили суть:
      Глобальное изменяемое состояние.


      1. SirEdvin
        05.01.2018 12:36
        +1

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

        Например, как решается ситуация, когда у моего приложения несколько потоков (по одному на алерт) и мне нужно одновременно отметить как отправленные/отмеченные два из них? Последовательно?


        1. Siborgium
          05.01.2018 13:04
          +1

          > я должен его каждый раз пересоздавать
          Формально — да. На практике все несколько иначе и зависит от реализации.
          А еще для последовательных вычислений и не-функционально-чистых операций используют монады.

          Но вообще говоря, если вы испытываете нужду в изменении глобального состояния, при этом используя функциональный стиль, то вы уже делаете все в корне неверно.


    1. life_observer
      05.01.2018 13:04

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


      1. SirEdvin
        05.01.2018 13:17
        +3

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

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


        1. Chaak
          06.01.2018 11:27

          Есть практики, типа в Go “Do not communicate by sharing memory; instead, share memory by communicating.”

          Данными, горутины(Golang)/процессы(Erlang, elixir) обмениваются через каналы/ящики, своеобразные входящие очереди, которые имеют своё собственное изолированное состояние.

          Все решается не управлением глобальным состоянием с алертами, а соблюдением single responisibility между ответственными за модификацию данных.

          Также есть практики типа redux’a в Js, основанных на реплейсом глобального стейта через чистые функции.


          1. SirEdvin
            06.01.2018 12:07

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

            А во втором случае опять заниматься синхронизацией изменений с разных потоков. И все это с довольно туманными целями. Потому что не понятно, что мне помешает поломать состояние при пересоздании так же, как и при модификации.


            1. Chaak
              06.01.2018 18:46

              Эти концепции позволяют в любое время масштабироваться. Для мелких целей есть мутекс.


  1. chicagoist
    05.01.2018 13:04
    +2

    Толку с того, что Clojure — ништяк? Рынок труда требует иных героев…


    1. dezconnect
      05.01.2018 16:06
      -1

      мы сами делаем выбор на чем писать.

      выбирать в 2018 PHP как минимум странно.


      1. iit
        05.01.2018 17:36
        +3

        А что плохого в php 7.x ?


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


        1. SerafimArts
          05.01.2018 20:53
          +1

          Есть =) Только о «тиках» и мультистримах мало кто знает. В любом случае на PHP можно, например, один раз запустить и два раза «умереть». Ну или внедрять определённый кусок кода (например профилер) раз в N тиков. На практике такие штуки (не тики, а стримы) используются, например в guzzle во время параллелизации асинхронных запросов (которые промизы возвращают).

          Ну и да, PHP многопоточный. Сборка TS об этом как бы явно говорит ;) Просто чаще используют NTS, с ними проще, да и это дефолтная поставка в виде fpm.


          1. iit
            05.01.2018 23:02

            Вы меня уделали. Совсем забыл об этих особенностях хотя guzzle довольно часто использую. Но сути это особо не меняет. Почему-то все относятся к php как будто он застрял на версии 5.3 и да, тогда он действительно был не самым лучшим языком. Но с php 5.4 и выше это уже вполне серьезный язык со всем что должно быть в серьезном скриптовом языке для web. Даже несмотря на то что js в данный момент дико популярен мне в нем действительно не хватает многих вещей которые есть в php из коробки.


            1. SerafimArts
              06.01.2018 00:07

              У PHP, имхо, никогда не было такой границы, где он был «серьёзным» или «игрушечным». Самый значимый прогресс самого языка, когда сама стилистика кода менялась, был в периоды php 4 -> php 5.3. Даже вот это упоминание «7.х» — это лишь в основном маркетинговые словечки для далёких от PHP. Всё что добавилось — это тайп-хинтинг для некоторых скаляров и анонимные классы, в остальном он ничем не отличается от того же, например, 5.6. В любом случае, стилистика кода никак не изменилась.

              Разве нет?


              1. iit
                06.01.2018 07:58

                Как раз такие мелочи вроде операторов ?? Или <=> плюс типы в том числе и возвращаемых значений когда их накопиться достаточно много и создают эффект изменения стилистики.


                Язык все ещё развиваться пусть и не такими ударными темпами как раньше.


                1. SerafimArts
                  06.01.2018 08:13

                  Да, null-coalesce (??) — это огонь. Без него уже как без рук, а на isset смотреть не могу уже =)


                  Но в целом я говорил о значимых изменениях как о "перевороте". Всё же эпоха php 4 и ранней 5ки открыли нам такие проекты, как вордпресс и битрикс, от упоминаний которых до сих пор содрогаются стены в офисах в разгар рабочих дней. И если во втором случае просто разрабы оной штуки просто… Кхм… Хз как культурно выразиться, не важно. То вордпресс изначально проектировался по принятым в те времена подходам. С другой стороны же можно сравнить какую-нибудь Symfony 2.x и Symfony 3.4+, требования PHP совершенно разные, но стилистика и идеи совершенно идентичные, потому что изначально писался адекватными людьми. Ну или Laravel 5.x, но там чуть сложнее, есть и адекватные сырцы, есть и дичь а-ля Yii (извини, SamDark )))).


    1. Simipa
      05.01.2018 16:39

      На всех найдётся работа. Я год назад перешёл на react native и забрал ту единственную вакансию что была.


  1. Tujh
    05.01.2018 16:37
    +1

    Цезура ради качества, свобода — это рабство, война — это мир.


  1. Sartor
    05.01.2018 17:27

    Автор считает, что все ошибки в программах — фатальные. Это совсем не так. Учитывая, что 90% кода пишется не в функцинальной парадигме и ПО постоянно становится лучше, то явно нет никакого безусловного преимущества функциональных языков. Да, иногда они лучше, для них есть ниша. Но это не универсальное решение всех проблем.


    1. worldmind
      05.01.2018 18:48

      Всё зависит от целей, если соседняя АЭС бахнет, потому что её ПО станет лучше только в будущем легче от этого вам не станет.
      Для игрового компа можно на чём угодно писать софт, но есть масса применений где важнее качество.


      1. struvv
        05.01.2018 19:29

        Но для АЭС будут писать код на Си тем не менее


        1. pftbest
          05.01.2018 20:58

          Скорее на MISRA C, а оно имеет мало общего обычным C.


        1. worldmind
          05.01.2018 21:18

          Были случаи?


      1. evseev
        07.01.2018 09:41

        Я в свое время участвовал в разработке ящика, который потом уехал на АЭС. Писали мы на С. Признаюсь честно. Мы очень старались не набокопорить ;)


  1. worldmind
    05.01.2018 18:53

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


    1. qw1
      05.01.2018 19:47
      +2

      Вопрос в том, почему хакеры, выбирая опасный инструмент, получают лучшие результаты, чем «обычные разработчики» с безопасным.


      1. worldmind
        05.01.2018 19:55
        +1

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


        1. qw1
          05.01.2018 21:20

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


          1. worldmind
            05.01.2018 21:37

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


          1. charypopper
            05.01.2018 22:34

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


          1. SirEdvin
            05.01.2018 22:42

            Все просто, потому что достижение безопасности чревато:
            1. Обрезанием функционала
            2. Уменьшением производительности.


  1. Oxoron
    05.01.2018 19:48

    Итак, ограничения делают всё лучше. Гораздо лучше.

    Отличный тезис. С одной стороны, разрушается абсурдным «Интересно, автор себя уже ограничил гробом?».
    С другой стороны, вызывает довольно интересное следствие: «Ограничения делают все лучше, так что ограничения стоит ограничивать.» Далее следует «Ограничивать ограничения ограничений»…

    Что до восхищения ФП — почему-то автор не указал ограничений его применения (вроде таких).


    1. worldmind
      05.01.2018 21:17

      > (вроде таких)

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


      1. Oxoron
        05.01.2018 23:05

        Да я тоже не спец. Проблема в том, что большинство статей ругающих ФП заостряют внимание на производительности. На чертовой производительности, которая колышет от силы 10% разработчиков (из них 9% используют либы созданные оставшимся процентом)! Допускаю, что во время зарождения ФП быстродействие было критичным для всех, но сейчас гораздо больше усилий уходит на анализ требований заказчика, чем на решение проблем с производительностью(по личному опыту).

        Получается интересная картина: недостатков (критичных) нет, ФП комьюнити потихоньку развивается\разрастается, но массовой миграции не видно. Почему?

        В довесок: я кодил настолки на ФП (успешно) и на ООП (не особо успешно). Видел нечитаемые WebAPI проекты на ФП и вполне приличные WebAPI проекты на ФП. Может выбор ФП-ООП не особо сильно влияет на успех проекта\читаемость кода?


        1. worldmind
          06.01.2018 10:15
          +1

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

          Таких «почему?» полно в ИТ, видимо отрасль сложная и за малый срок существования ещё не осилила определить хорошие практики, всё развивается стихийно, дурдома хватает.

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


  1. vanxant
    05.01.2018 20:01
    +3

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


    1. Oxoron
      05.01.2018 23:06

      vanxant а можно подробнее про недостатки?


    1. arXangel
      06.01.2018 00:25

      В javascript нет такого ключевого слова, но сам goto есть.

      Как пример: labeled statement
      developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/label


  1. lookid
    05.01.2018 20:56
    -1

    Потомучто программирование это бизнесс. И начальник хочет чтобы ты был гуру "его языка" и "его библиотек". Нечего сидеть на работе и JS изучать. Там драйвер у заказчика падает на железе с Win XP. Ограниченный программист под NDA — хороший программист.


  1. fshchudlo
    05.01.2018 21:57

    Со временем, новые языки программирования полностью отказались от поддержки GOTO.

    И добавили поддержку throw/catch. Структурно то же самое, но только без усов.
    Не раз и даже не два видел бизнес-логику, основанную на типах исключений. Да и сам пару раз такое писал, чего уж греха таить.
    Задумавшимся об альтернативах — ссылка на полезное видео


    1. evseev
      07.01.2018 09:50

      Я совсем не гуру, но даже для меня бизнес логика на исключениях- уже сильно перебор.


  1. kovserg
    05.01.2018 22:35

    Точно! Что бы софт был быстрым и не требовательным. Разработчиков надо заставить разрабатывать свои творения на железе прошлого века. Так сразу будет ощущаться вся боль и страдания. Но в результате на современном железе будет всё летать.
    На практике имеем обратную ситуацию.
    Главная цель не упростить жизнь программисту или получить самое эффективное решение, а прибыль. Так что победит решение предсказуемо позволяющее получать большую прибыль.


    1. qw1
      06.01.2018 10:19

      Разработчиков надо заставить разрабатывать свои творения на железе прошлого века
      Если применить к игровой индустрии, фирма получит продукт уровня Doom2 с бюждетом и сроками обычного AAA-проекта. Но зато летать будет на современном железе, это точно.


  1. stifff
    05.01.2018 22:44
    +1

    Но ведь закон Мура — он не производительность, а про количество транзисторов


  1. sumanai
    06.01.2018 03:07

    Итак, ограничения делают всё лучше. Гораздо лучше.

    Ограничили бы лучше статью от бессмысленных и не относящихся к делу изображений.


  1. graninas
    06.01.2018 12:46
    +1

    В русскоязычной среде все еще властвуют предубеждения и мифы о ФП. В то время как в западной культуре разработки ФП уже несколько лет мейнстрим. Я связывают это с незрелостью и малым бэкграудом многих разработчиков.


    Противники ФП повторяют раз за разом одни и те же мифы, абсолютно не понимая, что такое ФП. И часто они приходят к функциональщикам с императивным решением своей задачи вместо того, чтобы прийти с самой задачей, и ставят в вину функциональным языкам, что нельзя повторить императивное решение. Налицо XY-problem и непонимание базовых вещей дизайна ПО. То есть, такие вопросы задают незрелые кодеры, которые еще не вышли на уровень проектировщиков ПО, и которые мыслят категориями "как запрограммировать то-то и то-то", а не "какой дизайн и архитектура удовлетворят всем или почти всем требованиям?". И у стороннего наблюдателя создается ощущение, что у фпшников нет адекватных ответов. На самом деле, ответ на вопрос "как записать в БД значение, если в ФП нет побочных эффектов, а сама БД — это состояние, которого в ФП тоже нет" прост:


    • состояние есть, разные виды (чистое иммутабельное и нечистое мутабельное);
    • эффекты тоже есть, но контролируются;
    • возьми и запиши, как делаешь в обычных языках;
    • нет, от этого программа не перестанет быть написанной в функциональному стиле;
    • почему тебе нужно именно записать что-то в БД? Ведь это конкретное решение. Давай идти от задачи, а не от решения. Давай мыслить абстракциями, интерфейсами и паттернами вместо деталей реализации. Мы не решаем отдельную маленькую задачу записи в БД, мы пишем приложение целиком, где есть данные и их трансформация, и задача внести запись в БД несущественна на фоне задачи дизайна всего приложения. Может, в части общения с внешним хранилищем лучше подойдет специализированный DSL? Или STM? Или FRP? Или даже акторы/очередь/whatever? бе не подучить ФП, как ты учил ООП? Почему мы судим с позиции ООП/императивного программирования полноправную параллельную ветвь индустрии ПО? ФП тоже абстракция, тоже имеет свою область знаний, и тоже добавит в копилку новые инструменты дизайна ПО.

    Когда начинаешь мыслить такими категориями, когда уже по трогал не один язык и не одну парадигму, уже не возникает мысли "а как же производительность?" или "все равно все компилируется в императивным код", потому что пересекаешь черту зрелости, переходило из разряда кодеров в разряд инженеров, которые не задают вопросы "как сделать то-то и то-то", а идут и ставят эксперимент/делают прототип/изучают вопрос. Это — зрелый подход, и требуется время, чтобы к нему прийти.


    Мой доклад "Вы не понимаете ФП":
    https://youtu.be/jSkYvNqQWqs


    1. graninas
      06.01.2018 12:55

      Сорри за очепятки, эти экранные клавиатуры меня убивают.


    1. Oxoron
      07.01.2018 16:31

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

      Почему вы считаете, что зрелость\бекграунд зависят от географического положения? Будучи на западе я видел как абсолютных мамонтов, так и последователей ФП. При этом называть ФП мейнстримом я бы не рискнул.

      Вот корреляция между платформой и стилем есть, в Java ФП популярнее чем в .NET (по личным наблюдениям).

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

      Сильно не уверен, что подобные высказывания добавят убедительности аргументам. Ярлыки за гранью вежливости вызывают негативные эмоции, которые распространяются заодно и на ФП.

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

      В чем вы действительно правы — "Почему бы не подучить ФП, как ты учил ООП?". Стиль ведь действительно интересный.


      1. graninas
        07.01.2018 18:08

        Вы во многом правы, но кое в чем наши набоюдения расходятся. Говоря о западе, я имел в виду не совсем географическое положение, а, скорее, стиль мышления. Из личного опыта я вынес, что значительная часть русскоязычного программистского сообщества не готова к ФП и противится этим идеям. И говорю я, прежде всего, о плюсовиках, джавистах, php- и python- разработчиках (опять же, не всех). А вот JS-сообщество радует прогрессивностью мышления и широким использованием ФП-практик в повседневных задачах. О Java у меня другие сведения, а именно: с ФП там все плохо в сравнении с C#. Но если мы говорим о платформах .NET и JVM, то чаша весов на стороне последнего, благодаря большой популярности Scala и небольшой — Clojure.

        Почему я считаю, что на Западе ФП — стал мейнстримом? Потому что этой парадигме начинают отдавать предпочтение все чаще, рассматривая ее как равную параллельную ветвь, а не выкручивая кредит доверия в сторону ООП.

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

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

        А что касается сути, то будете ли вы оспаривать следующие утверждения: задающий вопросы, подобные моему — незрелый кодер; такой кодер не стал еще проектировщиком, мыслит менее абстрактными категориями; ФП — довольно абстрактно и потому кодеру доступны лишь его малые проявления (те же лямбды), но не функциональный стиль и не функциональный дизайн. Заметьте, в моих словах нет оценки «плохой/хороший», а есть оценка «еще не умеет/уже все умеет» (привет госпоже П.). Мы все такими были, но мы расширяем свою квалификацию, свой бэкграунд. Плохой кодер/разработчик — тот, что не учится.

        А знаете, что, по моим наблюдениям, мешает кодерам использовать даже самые простые идеи ФП, такие как иммутабельность и чистота? Вы не поверите: неспособность или нежелание писать чистый код. Да-да, тот самый, по Мартину и МакКоннелу. Люди хотят писать лапшу, потому что они так мыслят, и у них не возникает желания упростить себе работу в будущем. Часто им это и не нужно, потому что проблемы исправляются еще большим кодингом вместо переосмысления своих подходов и практик. ФП не терпит лапши, — если мы говорим о его правильном понимании и применении. Предвосхищая ваши мысли: я видел лапшу на функциональных языках тоже. Причин было ровно две: непонимание ФП с попыткой писать «как привык»; неверные дизайнерские решения по отношению к задаче. Первое исправляется " в человеке" более плотным изучением ФП, второе исправляется «в проекте» рефакторингом или редизайном. Но ничто, никакие практики не могут исправить кодера, пытающегося писать лапшу функционально и делающего вывод, что ФП — это лажа, а простые истины, как в статье — мировой заговор ФПшников. Нет смысла браться за ФП, если ты не готов изменить мышление и пополнить свой бэкграунд. Знаете, я начал писать чистый код только потому, что видел проблемы, к которым ведет нарушение простых принципов: DRY, SOLID, low coupling/high cohesion, и я совершенно логичным образом пришел к ФП, сам того не понимая (в качестве доказательства можете глянуть мой дипломный проект, про который я когда-то давно рассказывал на Хабре). Поэтому Haskell пошел как по маслу. Я уже умел избегать лапши в коде, а на это уже наложились новые подходы из мира ФП. И после этого уже стало очевидно, что best practices исповедуют те же принципы, что и ФП, только искаженные в свете ООП. Возьмите паттерны проектирования из GoF: да большая часть из них имеет функциональную природу, и решает проблему слабой выразительности конкретных языков. С применением ФП то же самое можно сделать в разы короче, чище, понятнее и композабельнее. Но ФП — гораздо, гораздо шире. И интереснее.


        1. Oxoron
          07.01.2018 20:09
          +1

          А знаете, что, по моим наблюдениям, мешает кодерам использовать даже самые простые идеи ФП, такие как иммутабельность и чистота? Вы не поверите: неспособность или нежелание писать чистый код.

          Вы хотите сказать, что код без иммутабельности и чистоты не «чистый»? Несколько категорично, не находите?

          я видел лапшу на функциональных языках тоже. Причин было ровно две: непонимание ФП с попыткой писать «как привык»; неверные дизайнерские решения по отношению к задаче.

          Поменяйте «Ф» на «ОО» — и фраза останется достаточно правдивой.

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


          1. graninas
            08.01.2018 09:46

            > Вы хотите сказать, что код без иммутабельности и чистоты не «чистый»? Несколько категорично, не находите?

            А в чем категоричность? Разве не очевидно, что в моем утверждении иммутабельность и чистота выступают примером и не являются полным охватом практик, которые приводят к чистому коду? Я могу назвать другие тоже: дробление задачи на маленькие кусочки; чистые функции там, где нужно преобразование данных; принцип единственной ответствености (SRP); принцип разделения ответственностей и выделение интерфейсов (ISP); Don't Repeat Yourself (DRY); и так далее. Можете ли себе представить, что люди не знают об этих принципах, даже проработав разработчиками много лет? И что самое печальное, они не приходят к ним естественным путем. Не нужно знать принцип SRP, чтобы сделать часть кода, ответственную только за одну задачу, ведь это упрощает использование кода, повышает его понятность и тестируемость. Однако, кодеры предпочитают отладку тестированию, что очень плохо. Отладки должно быть как можно меньше, ведь это показатель, что ты не понимаешь своего кода, какие там есть контракты и как он себя должен вести.

            И я вот это вот все вам рассказываю, но я уверен, что мы с вами понимаем, что это все совершенно тривиальные вещи. Иногда приходится жертвовать чем-то в угоду конкретных преимуществ, например, — в угоду производительности. Однако делать это нужно осознанно, ведь та же производительность нужна вряд ли больше, чем в 10% задач, а из них — вряд ли больше, чем в 10% кода. И нет никаких проблем писать код изначально чистым, или почти чистым. Это не приводит к дополнительной трате времени на разработку, — после определенного количества практики. Хотя поначалу, конечно же, следует изменить стиль мышления с «фигачения фич» на осознанный кодинг. Преимущества, я думаю, тоже ясны: у вас сразу же будут части программы, которые легче понимать, тестировать и рефакторить; будет возможность развивать отдельные компоненты без затрагивания соседних, потому что вы разделили ответственности и разделили также интерфейсы с реализациями; будет проще понимать и соблюдать контракты; будет возможность разделить тесты на юнит-, функциональные и интеграционные; вы начнете мыслить систематически и в терминах дизайна, а имплементация станет лишь следствием принятых решений. Как видите, преимуществ много, и не я их выдумал. Возьмите любые статьи по best practices, — там все это будет, но по каким-то причинам некоторой части наших коллег это неинтересно.

            И да, это касается в равной мере ООП и ФП. Лучшие практики потому и требуют изучения, что они универсальны, переносимы с одной парадигмы на другую. Создавая большой проект, вы не сможете обойтись без этих практик, потому что иначе проект будет обречен с самого зарождения. Не берем в расчет маленькие проекты: к ним другие требования, и жизненный цикл их иной. Однако, и маленькие проекты могут быть теми кошками, на которых следует тренироваться.

            Что касается ФП, то оно принуждает использовать практики. Если вы пишете императивно на ФЯ, или пишете лапше-код — вы что-то делаете не так, и это не ФП.

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

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

            В ФП никак нельзя нарушать принцип SRP, потому что функция не будет композироваться с другими. А лапше-код именно такой: в нем все намешано, порой много разных факторов взаимодействуют нетривиальным образом, — и если создать такую функцию, она не будет ни с чем компоноваться. Она будет миром в себе, этаким деревянным кубиком в груде конструктора Лего, который ни к чему толком не подходит.

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

            Собственно, гарантии, приходящие из чистой части кода, крайне облегчают его дальнейшее сопровождение. И, пожалуй, это наиболее полезное следствие ФП, отличающее его от ООП. Ведь ООП — императивно по своей сути. Без тех же явных контрактных аннотаций или юнит-тестов ваш код не будет давать никаких гарантий; да и с ними может произойти так, что в коде закрался эффект, не видный из тестов (если они вообще есть), но влияющий на соседние компоненты во время выполнения программы. ООП ограничивает программиста, предлагая больше гарантий в сравнении с простым императивным кодом, но ООП остается императивным. ФП идет еще дальше и поднимает вопрос гарантий в плоскость математически обусловленных свойств кода, а математика, как известно, — самый сильный гарант гарантий.

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

            Что касается недостатков, они тоже есть. Вам придется платить тем, что вы уже не сможете просто фигачить фичи. А значит, какое-то время ваша продуктивность упадет, но качество кода начнет возрастать, — даже не на ФЯ. Разве вы никогда не занимались поддержкой жуткой лапши legacy, пришедшей из тех времен, когда практик еще не было? Иногда нет выбора, и поддержка legacy критична для бизнеса. Но сейчас совершенно нет никаких причин игнорировать практики при разработке новых больших проектов. Ведь выросла потребность и в командной разработке, а в ней понимание кода другими играет определяющую роль. Да, вы будете писать проект чуть медленнее. Но вы значительно выиграете время на его развитии и поддержке, не говоря уж о радикально сниженном количестве багов. ФП энфорсит корректность кода на многих уровнях, в отличие от того же ООП.

            Я не буду говорить про производительность. Бегать с бешеными глазами и кричать о том, что теряется производительность, — это, ей-богу, ребячество. Как уже было много раз говорено, ваша задача вряд ли включает в себя требование hard или soft real time производительности, — а для всего остального ФП может подойти так или иначе. В конце концов, почему мы так необъективно забываем, что пишем на очень медленном Python? Все к нему привыкли? Но стоит заговорить о ФП, как производительность становится краеугольным камнем, на который давят все противники, даже питонисты. Но если разобраться, ФП не такое уж и медленное, а с учетом прекрасной поддержки параллелизма, оно еще и сильно выигрывает. Если бы мне предложили выбор с точки зрения производительности: Python или любой ФЯ, я бы не сомневаясь выбрал последнее.

            Конечно же, изучение ФП требует времени. В ФП есть несколько уровней сложности, и эта тема более глубока, чем ООП (включая как классическое C++/Java ООП, так и исконное Алан Кеевское). Практики и подходы, паттерны проектирования и целые принципы построения дизайна (те же FRP или STM), — все это требует изучения, если вы начнете погружаться глубже и захотите перейти от кодера к человеку, мыслящему системно. Но пусть так: вам это не нужно, вы никогда не будете писать все в функциональном стиле, вам не нужно использовать ФП в повседневной разработке. Тем не менее, изучение даже базовых вещей из мира ФП сделает из вас более хорошего разработчика. И это на фоне того, что даже такие консервативные мастодонты как Java и C++ вносят в свой мир элементы ФП. Как вы думаете, почему это происходит?


            1. Oxoron
              08.01.2018 10:53
              +1

              Если вы пишете императивно на ФЯ, или пишете лапше-код — вы что-то делаете не так, и это не ФП.

              Проблема в том, что если я не пишу императивно на ФЯ и не пишу лапше-код — я по-прежнему могу делать что-то не так, и это не будет ФП. Кроме того, вы даете неюзабельное определение: «это не ФП».

              Кроме того, я вас не понимаю. С одной стороны, вы утверждаете, что в ФП нельзя нарушить SRP. С другой стороны, сразу же показываете как это сделать («если создать такую функцию, она не будет ни с чем компоноваться.»).

              Что получается в итоге: «дробление задачи на маленькие кусочки; принцип единственной ответствености (SRP); принцип разделения ответственностей и выделение интерфейсов (ISP); Don't Repeat Yourself (DRY); и так далее» — это принципы применимые и в ООП. (Я исключил из вашего списка чистые функции, хотя они в ООП не запрещены). Лапше-код можно написать на любом языке. Подавляющее большинство задач можно сделать красиво, лаконично и неправильно как с ФП, так и с ООП. При этом ФП запрещает «фигачить фичи» (на самом деле нет), то есть обладает как минимум одним недостатком близким к фатальному.

              По поводу производительности — да, ныне про неё можно забыть (хотя бенчмарки придумать сложно). 90% задач производительности не требуют.

              В итоге из ваших комментариев не вполне ясны преимущества ФП. То есть, ограничения уже есть, а преимущества туманны.

              P.S. Мы уже наваяли тонны текста. Предлагаю перенести дискуссию в личку.


              1. graninas
                08.01.2018 11:20

                Я дал юзабельное определение ФП в своем докладе, на который сослался в первом сообщении. Точнее, не определение, а набор признаков. Могу повторить здесь. Имеется три уровня понятия «Функциональное программирование»:

                — Элементы ФП. Лямбды, иммутабельность, чистота, алгебраические типы данных, первоклассные функции и функции высших порядков. Это только элементы, а не всё ФП. Вы можете написать код с лямбдами и иммутабельностью, но он по-прежнему будет содержать шаги/инструкции вместо деклараций.

                — Функциональный стиль. Здесь начинается настоящее ФП, а именно: композиция функций, каррирование, декларативность, частичное применение. В функциональном стиле можно писать чистый код и код с эффектами.

                — Функциональный дизайн приложения. Системы типов, системы эффектов, предметно-ориентированные языки, ФПшные подходы, паттерны и идиомы. Вы строите приложение целиком в функциональном стиле и с применением best practices.

                > С одной стороны, вы утверждаете, что в ФП нельзя нарушить SRP.
                Нет, позвольте. «Нельзя нарушать» (в смысле — запрещено) и «нельзя нарушить» ( в смысле — невозможно) — разные вещи, и я написал верно. Сломать ФП-код можно, но не нужно.

                Я не претендую на полное изложение преимуществ. Есть такое парадоксальное когнитивное искажение: чем больше приводится аргументов, тем оппонент менее склонен их воспринимать, и он неосознанно еще больше убеждается в своей правоте. Поэтому, возможно, статьи о преимуществах ФП выглядят для противников неубедительными. И поэтому я предлагаю ставить эксперименты.

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

                Как я уже говорил, принципы универсальны. Но ООП не энфорсит их соблюдать, — да и само ООП может пониматься крайне различными способами. А когда вы начнете применять принципы к ООП коду — те же SOLID, — вы придете, внезапно, к ФП-коду, только перегруженному синтаксическими конструкциями из ООП мира. В ООП много ненужной мишуры, много ритуалов и синтаксических приседаний. Это все ведет к перегруженности кода, к тяжелому восприятию, к необходимости помнить о десятках элементов, не относящихся к предметной области, но используемых, чтобы соблюсти те или иные контракты. Возьмите хотя бы область видимости полей класса и модификаторы доступа. Это все — ненужный мусор, без которого можно обойтись, разделив ответственности на уровне модулей, и исключив глобальное мутабельное состояние. В итоге ФП код становится в гораздо большей степени про предметную область и бизнес-логику, чем про языковые конструкции. И это все ведет к снижению accidental complexity, — к снижению привнесенной сложности, что, в свою очередь, является главной задачей дизайна ПО.

                P.S. Мне кажется, мы с вами хорошо беседуем. Я понимаю, что вам большая часть всего, что я говорю, хорошо известна. Поэтому я воспринимаю наш с вами диалог, скорее, как интервью, и рассказываю это все для потенциальных читателей.


                1. qw1
                  08.01.2018 12:24

                  Если говорить о SOLID, для меня неясно, как работает Dependency Inversion и Interface Segregation в ФП (и вообще, что есть аналог интерфейса в ФП). У меня впечатление, что ФП-шники на это не обращают внимание, концентрируясь на SRP из всего SOLID.

                  Есть ли у ФП-программы точка сборки (composition root)?

                  Может, есть статьи по этой теме?


                  1. graninas
                    08.01.2018 14:24
                    +1

                    Хороший вопрос. Начнем говорить о IoC/DI/ISP/интерфейсах с того, что есть два аспекта, а точнее, два вида полиморфизма:

                    — Динамический полиморфизм;
                    — Статический полиморфизм.

                    Динамический полиморфизм принят в «классическом» ООП, и это, пожалуй, самая главная его черта. На основе наследования можно разделить интерфейсы и реализацию, а в таких языках как Java и C# даже введены специальные синтаксические конструкции для этого. Динамический полиморфизм такого вида позволяет определить интерфейс в одном модуле трансляции, а реализацию — в другом модуле трансляции, и они могут быть скомпилированы раздельно. Мы пользуемся этим в подгружаемых библиотеках .NET и Java, реализуя предоставляемые ими интерфейсы.

                    Еще к динамическому полиморфизму можно отнести duck typing в динамических языках программирования. И с этим тоже не возникает проблем: мы реализуем inversion of control, используя duck typing. Это работает, например, в Python. Мы можем понимать класс как принадлежащий тому или иному интерфейсу, если в нем есть требуемые методы.

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

                    Статический полиморфизм работает иначе. В нем полиморфное поведение реализуется на уровне типов, когда есть некое обобщение типов, под которое попадают другие пользовательские типы. Генерики, шаблоны С++ и система типов Haskell — именно об этом. Полиморфный тип может пониматься как интерфейс для более конкретных типов. Функции высших порядков — те, что принимают другие функции на вход — могут пониматься как полиморфные, а их тип задает интерфейс. Таким образом, Inversion of Control зашит в ФП на уровне концепции, потому что в ФП вы передаете одни функции в другие, лишь бы тип принимающих функций (функций высших порядков) был достаточно общим для этого. Но у статического полиморфизма есть определенная проблема, доставляющая много неудобств в таких языках как Haskell, где система типов чрезвычайно строгая. Мы рассмотрим эту проблему чуть ниже, а сначала позвольте привести пример простого статического полиморфизма и оного же на основе классов типов.

                    Мы хотим взять список и вычислить какую-нибудь среднюю величину по всем элементам. Задача такого рода типична в мире ФП, поэтому она носит название «свертка». Например, свертка списка к сумме его элементов. Или к произведению. Или к другой структуре данных, например свертка списка натуральных чисел к списку четных чисел. Мы ограничимся только суммой. Функция свертки списка (в C++ — accumulate, в Haskell — fold, а есть еще reduce) ожидает на вход бинарный оператор, в нашем случае — оператор суммы (+). Функция свертки foldr в Haskell регламентирует то, что в нее нужно передать:

                    foldr :: (a -> b -> b) -> b -> [a] -> b

                    Здесь (a -> b -> b) — это бинарный оператор (например, лямбда суммы двух чисел), b — начальное значение (аккумулятор, у нас будет 0), а [a] — список из значений типа a. На выходе — значение типа b. Поскольку foldr полиморфен статически, на этапе компиляции в него будут подставлены наши конкретные типы, и получится такая неполиморфная функция:
                    foldr :: (Int -> Int -> Int) -> Int -> [Int] -> Int

                    Мы будем вызывать foldr с нашим оператором (+), имеющим как раз тип (Int -> Int -> Int), и это будет классический пример Inversion of Control, только не содержащий сторонних эффектов. А типы здесь выступают как интерфейсы.

                    Однако, статический полиморфизм более разнообразен, и классы типов (не путать с классами из ООП) позволяют реализовать IoC на еще более генеричном уровне. Приведенная выше функция foldr была полиморфной по оператору свертки, но она не была полиморфной по контейнеру, над которым производится сумма. Нам посчастливилось использовать список, а что если мы не знаем, какой у нас будет контейнер в будущем? Например, мы пишем библиотеку, а пользователи определяют свои коллекции? Тогда нам поможет еще более полиморфная функция:
                    foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b

                    И здесь уже появляется класс типов Foldable под сокращением t. Он говорит, что не важно, какая у вас коллекция t a, лишь бы для нее был реализован интерфейс свертки Foldable. Это может быть список, массив, граф, — что угодно. Класс типов здесь выступает интерфейсом для данных, хотя может выступать интерфейсом для поведения тоже. Примечательно, что в С++ ожидается нововведение под названием «концепты», которое является классами типов. И это нововведение сильно упростит многие библиотеки, включая Boost.

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

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

                    Давайте рассмотрим эту тему подробнее. Обычно интерпретируемые языки создаются с помощью алгебраических типов данных (ADT). Конструкторы ADT будут вашими методами, а сам тип будет интерфейсом к вашей подсистеме. Очень упрощенный пример:

                    data AccountOperation
                    = PutMoney AccountNumber Money
                    | WithdrawMoney AccountNumber Money


                    Используя этот язык, вы пишете сценарии, которые зависят только от интерфейса. Это аналог клиентского кода, который использует интерфейсы в Java или C#. Разница лишь в том, что в ООП под интерфейсом скрыта конкретная реализация, а под интерпретируемым языком ничего не скрыто. На псевдокоде сценарий мог бы выглядеть так:

                    putAndGetMoney :: [AccountOperation]
                    putAndGetMoney =
                    [ PutMoney "1234" 100500
                    , WithdrawMoney "1234"
                    ]

                    Вы пользуетесь декларациями методов вашего языка и определяете, что должно произойти. Но этот декларативный сценарий еще нужно заставить работать, — и здесь появляется этап интерпретации. Вы создаете интерпретаторы для различных условий, например, для разных банков. При этом интерпретаторы могут быть нечистыми, например — ходить в сторонние сервисы или в БД. Вот как мог бы выглядеть интерпретатор:

                    interpret :: AccountOperation -> InputOutput
                    interpret (PutMoney number amount) = SberbankAPI.put number amount
                    interpret (WithdrawMoney number amount) = SberbankAPI.withdraw number amount


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

                    Приведенный выше пример языка очень ограничен в плане выразительности. Он не отслеживает ошибки, тип операции слишком узок и не учитывает возвращаемых значений, а набор методов определенно мал. Чтобы сделать использование этого подхода еще более полезным, существуют специальные функциональные паттерны. Один из них — свободные монады (Free monads). И у меня даже есть три примера из реальной практики, когда свободные интерпретируемые языки с успехом применялись в ФП-коде для разделения интерфейсов и реализации.

                    Кроме того, существует ряд других способов сделать DI в строго функциональном стиле. Я не буду вдаваться в подробности, а отправлю вас к своей (недописанной) книге, где я посвятил этому целую четвертую главу. Материал доступен онлайн: Functional Design and Architecture.

                    А про точку сборки я не имею представления, так как сталкиваюсь с понятием впервые. Либо позвольте мне сперва изучить вопрос, либо расскажите, что это в мире императивного программирования.


                    1. qw1
                      08.01.2018 19:10
                      +1

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

                      DI требует, чтобы классы зависели от абстракций, а не от других классов.
                      Для этого, вместо зависимого класса используют интерфейс, а экземпляр зависимого класса создаётся где-то снаружи и обычно передаётся через конструктор:
                      public class SberbankService : IBankService {
                         public SberbankService(IHttpClient htppLib) { ... }
                      }

                      Так вот, точка сборки — это место в коде (выполняющееся обычно при старте приложения), где всём абстракциям назначаются реализации:
                      IHttpClient http = new HttpLib();
                      IBankService bank1 = new SberbankService(http);
                      IBankService bank2 = new AlphabankService(http);
                      IBankService loggedBank2 = new GenericLogger<IBankService>(bank2);
                      ITransferService tr = new TransferService(bank1, /*bank2*/ loggedBank2);
                      ...
                      tr.MoveMoney(acc1, acc2, 100500);

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

                      С учётом написаного вами выше, понятно, что такое можно сделать и в ФП, если передавать во все функции первыми параметрами зависимые функции, а далее делать частичное применение, фиксируя первые параметры.

                      Вопрос тогда трансформируется в следующий: а в ФП-проектах такое в принципе практикуется
                      1. все композиции фунцкций делать в одном месте
                      2. все функции бизнес-логики никогда не вызывать напрямую, а принимать их как дополнительный параметр, и вызывать только через принятый параметр.
                      в п.2 особый акцент на слове *все*, т.к. если где-то забыл об этом принципе, считай, что DI сломана.

                      И ещё мне показалось (это не проблема ФП в целом, а проблема Haskell), что все эти Int->(Int->Int->Int)->Int добавляют когнитивной нагрузки при написании и чтении кода. Тут будет придерживаться SOLID только потому, что иначе любое описание функции утонет в 50 стрелочках. Причём в ООП это будет код с душком, но в принципе поддерживаемый.

                      С интерпретаторами как-то очень сложно.
                      Даже на простом примере без них нельзя?
                      Вот, к примеру есть такая абстракция:
                      interface ISummator { int sum(a,b); }
                      interface IMultiplier { int mul(a,b); }
                      int dotProduct(ISummator s, IMultiplier m, Point x, Point y);


                      Можно сделать
                      dotProduct :: (int->int->int)->(int->int->int)->p->p->int
                      Но как защититься от неверного использования, когда вместо summator передали multiplier, и наоборот.

                      Можно ли к (int->int->int) сделать какую-то метку, чтобы туда подходили не все функции, а только специально помеченные, как совместимые на это место?


                      1. graninas
                        08.01.2018 20:30

                        Спасибо за разъяснение. Значит, то место, где я заполнял IoC контейнеры, имеет свое название — «точка сборки». В ФП-программе мы делаем точно так же. В главной функции main мы определяем конкретные реализации и передаем их как зависимости в клиентский код. И да, мы это делаем в настоящем проекте, не в придуманном.

                        Я попробую ответить на ваш вопрос, хотя не очень понимаю словосочетание «все композиции в одном месте». Если бы мы заменили слово «композиции» на «настройку» или «инстанцирование», — было бы лучше?

                        Чтобы количество стрелочек в аннотации типа не превышало все разумные пределы, мы воспользуемся алгебраическим типом данных. Пусть этот тип содержит все возможные зависимости, то есть, он будет являться аналогом IoC-контейнера. Я назову этот тип Runtime:

                        data Runtime = Runtime
                            { summator :: Int -> Int -> Int
                            , multiplier :: Int -> Int -> Int
                            }


                        Полей для разных зависимостей может быть сколько вам нужно. Настроить Runtime мы можем, например, так:

                        main = do
                           let runtime = Runtime {summator = (+), multiplier = (*)}
                           dotProduct runtime (Point 1 2) (Point 2 3)


                        Передаем этот «контейнер» как аргумент, и все его содержимое становится доступно функции:
                        dotProduct :: Runtime -> Point -> Point -> Int
                        dotProduct (Runtime sum mul) p1 p2 = mul (sum 3 5) 10
                        -- result: 80


                        Вы спрашиваете, как пометить (Int -> Int -> Int). Конечно же, это можно сделать, — и несколькими способами. Начнем с алгебраических типов-оберток, в которые положим функции суммирования и умножения:

                        data Summator = Summator (Int -> Int -> Int)
                        data Multiplier = Multiplier (Int -> Int -> Int)
                        
                        dotProduct :: Summator -> Multiplier -> Point -> Point -> Int
                        dotProduct (Summator sum) (Multiplier mul) p1 p2 = mul (sum 3 5) 10


                        Но это не вполне убедительно, так как функции sum и mul, лежащие в алгебраических типах данных, все равно можно спутать в клиентском коде. Попробуем сделать так, чтобы этого не случилось. Сделаем интерпретируемый язык.

                        data BinOperation
                           = Sum Int Int
                           | Mult Int Int
                        
                        type Interpreter = BinOperation -> Int
                        interpret :: Interpreter
                        interpret (Sum x y) = x + y
                        interpret (Mul x y) = x * y
                        
                        dotProduct :: Interpreter -> Point -> Point -> Int
                        dotProduct eval p1 p2 = eval (Mul (eval (Sum 3 5)) 10)


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

                        Я еще не рассказал, как можно для тех же целей использовать классы типов, Free monads и обобщенные алгебраические типы данных (GADTs). Если желаете погрузиться в Haskell еще больше, — расскажу :)


                        1. graninas
                          08.01.2018 20:42

                          Я немного поторопился.
                          > только функции будут уже нечистыми, как в нашем примере.
                          Не как в нашем примере.


                        1. qw1
                          08.01.2018 23:18

                          Спасибо за разъяснение. Значит, то место имеет свое название — «точка сборки»
                          В книге Симана Марка «Внедрение зависимостей в .NET» можно прочесть:
                          Слабое связывание позволяет писать код, который допускает расширяемость, но не позволяет вносить изменения. Такой подход называется принципом открытости/закрытости (Open/Closed Principle). Единственное место, требующее модификации кода, — это стартовая точка приложения, называемая корнем сборки (Composition Root)
                          «Точка сборки» — вероятно, хабражаргон, который я встречал в нескольких статьях (например) и во многих комментариях.

                          Что меня озадачило, так это фраза «место, где я заполнял IoC контейнеры».

                          В моём примере выше composition root написан вручную, без IoC-контейнера. Контейнер же — это довольно сложная библиотека, которой скармливаешь пары [интерфейс, реализация] и дополнительную информацию (время жизни, стратегия создания экземпляра) и т.п., а эта библиотека, когда её попросишь создать какой-то экземпляр, автоматически строит граф зависимостей и создаёт экземпляр и все зависимости в соответствии с настройками.

                          Обычно контейнер использует рефлексию, поэтому я очень удивился, обнаружив упоминание контейнера в контексте ФП.
                          Я назову этот тип Runtime
                          Блин, вот что значит зацикленность на стереотипах. АТД я мыслил типа как record в Паскале. А то, что можно описать функцию, вместо привычных полей с данными, как-то даже и не подумал. Тогда вообще никаких проблем с интерфейсами.

                          Но это не вполне убедительно, так как функции sum и mul, лежащие в алгебраических типах данных, все равно можно спутать в клиентском коде.
                          Попробую по-другому сформулировать, что я хочу. Например, у меня есть 5 функций sum1, sum2, sum3, sum3, sum4, sum5 и 3 функции mul1, mul2, mul3 с одинаковым типом int->int->int. Можно ли написать такую ф-цию
                          dotProduct :: X->Y->Point->Point->int
                          (подставив вместо X и Y какие-то ограничения), чтобы компилировался вызов, только если первый параметр dotProduct — одна из ф-ций sum1-sum5, а второй параметр — mul1-mul3?

                          Попробуем сделать так, чтобы этого не случилось. Сделаем интерпретируемый язык.
                          Интерпретатор, как я понимаю, не позволит в dotProduct перепутать Sum и Mul. Но мы этого уже добились, введя тип Runtime, и присвоив правильное значение переменной runtime. Что меняется с вводом интерпретатора, для этого примера он избыточен?

                          По интерпретаторам интересно узнать:
                          1. Интерпретация — это медленно?
                          2. Насколько громоздко описывать интерпретаторы? Верно ли, что выше приведён кусочек, иллюстрирующий идею, а для запуска надо ещё написать много-много строчек?


                1. Oxoron
                  08.01.2018 12:27

                  Определения — ок.

                  Сломать ФП-код можно, но не нужно.

                  Согласен. Тот же подход можно применить и к ООП.

                  Есть такое парадоксальное когнитивное искажение: чем больше приводится аргументов, тем оппонент менее склонен их воспринимать.

                  Согласен. Стараюсь быть аккуратнее (вроде взаимно). Следствие: нет резона объяснять ФП в статьях? Какой формат может быть лучше?

                  Там, где кодер пойдет фигачить фичу, профессиональный разработчик подумает о всех крайних случаях, о требованиях, и прошерстит набор своих инструментов в поисках наиболее удовлетворяющего требованиям.
                  Аналогично для ООП.

                  В ФП это становится более важным, потому что писать наивный ФП код чревато многими неприятными последствиями.
                  То есть, ООП безопаснее-по-дефолту? Меньше порог входа?

                  Возьмите хотя бы область видимости полей класса и модификаторы доступа. Это все — ненужный мусор...
                  Спорно. Трудно доказуемо. Кроме того, модификаторы доступа — один из первых элементов ООП (по крайней мере, один и самых употребительных). Безотносительно истинности аргумента — его тяжело принять.


                  1. graninas
                    08.01.2018 14:38

                    > Следствие: нет резона объяснять ФП в статьях? Какой формат может быть лучше?

                    Будем рассматривать это следствие для комментариев и комментирующих. В статьях, конечно же, говорить об этом нужно. А в ленивому комментатору, возможно, проще сделать ссылку.

                    Я не уверен, что значит «безопаснее по-дефолту». Мне также трудно оценить порог входа в ООП. Есть разные уровни сложности и в ООП, и в ФП. Новичок не сможет задизайнить систему хорошо ни там, ни там. Ему придется изучить как философию, так и подходы, принятые в парадигме. Есть свидетельства, что в ФП материала больше на всех уровнях, но его понимание и освоение может протекать также гладко, как и для ООП. У меня есть примеры, когда вчерашние студенты через две недели изучения уже начинали писать код на Haskell. Пусть простой, но все-таки функциональный. Мне сдается, что сложность ФП преувеличена. И об этом я тоже говорил в докладе.

                    > Спорно. Трудно доказуемо. Кроме того, модификаторы доступа — один из первых элементов ООП (по крайней мере, один и самых употребительных). Безотносительно истинности аргумента — его тяжело принять.

                    Не проблема, пусть останутся моменты, на которых мы расходимся.


  1. xFFFF
    06.01.2018 17:37

    Как инженер, не согласен со многими высказываниями.


  1. eshirshov
    06.01.2018 19:05

    Самый яркий пример само-ограничений (для c, c++), который я знаю-MISRA. Кровавые слёзы промышленного программирования так сказать.


  1. Laryx
    06.01.2018 20:54

    По мне — так ограничения необходимы лишь для того, чтобы упрощать себе жизнь.

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

    Или, скажем, константные функции нужны, чтобы нельзя было случайно внести изменения в объект.

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


  1. amakhrov
    07.01.2018 06:12

    Вводная часть статьи, где говорится про ограничения — передергивания и фактические ошибки. Неконструктивно :)


    Аналогия с музыкой.


    То есть ограничения могут делать предмет более притягательным.

    (А могут и не сделать)
    В программировании не стоит задача сделать код притягательным. Код должен быть понятным. Поэтому применимо к программированию "всё остальное до отвращения очевидно" явно является предпочтительным вариантом.


    Аналогия с фильмом Челюсти.
    Это аналогия того же уровня, что и предыдущая. Что хорошо в кино, необязательно хорошо в разработке ПО. Как говорится, "плохая аналогия подобна котенку с дверцей".


    Итак, ограничения делают всё лучше. Гораздо лучше.

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


    Пример с художником.
    Опять же, некорректная аналогия. В разработке редко стоит вопрос "что сделать". Вопрос стоит "как сделать".
    Можно привести контр-пример. Заказчик говорит: "Мне нужен такой-то продукт, используй для него PHP". PHP — ограничение. Делает ли оно решение задачи проще? Может быть — если PHP действительно хорошо подходит для этой конкретной задачи. А если не очень, то это ограничение только мешает.


    Работа с "железом".


    ограничения делают работу с аппаратным обеспечением проще, чем работа с ПО

    Ну да, ну да. Физические ограничения, наоброт, все усложняют. Борешься с одним ограничением — оп! уперся в другое. Насколько было бы проще разработать процессор, если убрать, скажем, ограничение по температуре / теплоотводу.
    Ну или я просто совсем не понял мысль автора в этом пункте.


    В программах можно все.
    (1)


    никакие ограничения не накладываются на то, что она может делать с принадлежащими ей кодом и данными.

    (2)


    Именно отсутствие ограничений делает написание и поддержку ПО таким сложным делом.

    Отсутствует аргументация для перехода от (1) к (2).
    Я так же безапелляционно могу заявить, например:
    "Программы пишутся людьми. Именно человеческий фактор делает написание и поддержку ПО таким сложным делом."
    Или же
    "Программы моделируют реальный мир. Именно разница между реальным миром и машинной моделью делает написание и поддержку ПО таким сложным делом."
    Или даже
    "Программы работают с данными. Именно это делает написание и поддержку ПО таким сложным делом".
    Или… ну, вы поняли.


    Пример с GOTO, который "ограничили".


    Даже в этом примере, видно, что на самом деле индустрия выиграла не от введения ограничений (как уже писали выше, goto все так же присутствует во многих языках), а от добавления новых возможностей (напр., явные управляющие конструкции для циклов).
    Да банально взять любой ЯП и посмотреть, как он развивался со временем. В большинстве случаев это добавление новых возможностей, а не постановка дополнительных ограничений. Неужели это делает ЯП хуже, а написание програм сложнее?


    Ну, а если вводную часть про ограничения убрать из статьи, то на выходе получится очередная одна статья из разряда "ФП — лучше всех!" :)


  1. evseev
    07.01.2018 10:21
    +1

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

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


    1. qw1
      07.01.2018 10:52

      Тут важный момент — любой ли модуль может получить доступ к любой глобальной переменной?

      А то ведь загрузится модуль, попортит данные всем другим модулям, и дальше что делать? Заменить один этот модуль на исправленный недостаточно. Только систему перезагружать


  1. evseev
    07.01.2018 16:56

    Да. Любой модуль имеет доступ к любым переменным. Пользователь с достаточными правами в системе может прочитать или установить любую переменную.

    С какой это стати любой из модулей будет портить данные? Вы в своих программах портите «чужие» данные просто потому, что можете?

    И вы были невнимательны. Модуль менять не нужно. Изменения вносятся в работающий модуль.

    И нет. Систему перезагружать не нужно.


  1. sultan99
    08.01.2018 14:26

    Мне кажется, что именно функциональный подход накладывает больше вычислительных и аппаратных ресурсов нежели ООП-шный, поправьте если я ошибаюсь.


    К примеру, приложение типа Todo состояние, которого представлен в виде массива из 50 записей, и вот мне надо одну строчку пометить как "выполнено", мне необходимо сделать новую копию 50 записей, заменив значение на true, то самой строчки.


    Разве это оптимальное решение, о котором говорится в последнем пункте статьи? Тут затраты процессора на перебор, тут память выделяется под новые записи и только для того, чтобы поддержать иммутабельность состояния приложения. Вот у меня операционка заняла память 4ГБ, и надо поменять какое-то состояние и ...


    Еще один момент, в статье говорится, что глобальное состояние это зло, но в функциональном программирование, состояние это один глобальный стор (я тут сравниваю Редакс), когда в ООП-шном подходе состояние раздроблено и спрятано в компоненты/модели — одним словом в объекты и нет никаких глобальных состояний.


    Чего-то я не до понимаю, хотя очень стараюсь, не получается вникнуть в эту новую философию программирования.


    1. graninas
      08.01.2018 14:41

      Возможно, вам поможет мой доклад «Вы не понимаете ФП»:
      youtu.be/jSkYvNqQWqs


    1. vchslv13
      08.01.2018 15:19

      «Я сделяль...» :)

      data Task = Task { t_id::Int
                       , title::String
                       , text::String
                       , is_finished::Bool
                       } deriving Show
      
      myTasks = [ Task 0 "Sleep" "Have a sleep" True
                , Task 1 "Work" "Make some work" False
                , Task 2 "Fap on Haskell" "..." False
                , Task 3 "Something else" "..." True
                ]
      
      setFinished :: Int -> [Task] -> [Task]
      setFinished task_id = map (\t@(Task tsk_id ttl txt _) ->
                                     if t_id t == task_id
                                     then Task tsk_id ttl txt True
                                     else t)
      
      -- set task as finished (returns new list with tasks)
      setFinished 2 myTasks
      


      Я тоже начинающий, но, надеюсь, меня за этот пример не забьют палками.
      Так вот, его суть в том, что мы задаём тип данных Task, который имеет «поля» для хранения id, названия, текста и завершённости задания. Потом создаём список myTasks с четырьмя заданиями.

      Функция setFinished — это самая примитивная функция, которая выполняет то, что Вы хотите — «помечает» задание с требуемым id как «выполненное» (в данном случае вызов «setFinished 2 myTasks» возвращает новый список задач, где задача 2 выполнена). Как видите (ну или не видите, что тоже весьма вероятно XD) мы не делаем копии всех имеющихся записей. Мы только пересобираем основной список (N элементов) и ту запись, которую нам нужно изменить. Остальные, насколько я понимаю, остаются нетронутыми.

      Для сравнения можете представить, что у вас есть односвязный список в С++ (например), каждый узел которого хранит указатель на объект типа Task. Даже при необходимости соблюдения иммутабельности объектов, Вы не будете делать копии «всех 50 записей» — Вы просто скопируете указатели 49 записей в новые узлы списка и создадите новый объект изменяемой записи.

      Как-то так, в общем… Надеюсь, всё правильно расписал.


  1. terra-slav
    08.01.2018 14:26

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