Comments 113
Может хватит уже вести вечный холивар на тему «ООП отстой! Нет ФП отстой!», а понять наконец что для своей задачи свой инструмент. Доведение до абсурда ещё ни кого не приводило к хорошему результату.
даже функции высокого порядка в Java. Так выглядит Java в 2018-м… Функциональное программирование настолько полезно и удобно, что, насколько я вижу, проникло во все современные распространённые языки.
Сформулировано впечатляюще.
Но… «Функции высокого порядка» умели еще Pascal и C, еще порядка полувека тому назад.
А возможно и более древние языки.
Проникло в современные языки, говорите?
Все новое — хорошо забытое старое.
users.stream().map(user -> user.getUserName()) — какого тут типа результат? А теперь все тоже самое про C и Pascal расскажите.
Я это вполне могу написать на плюсах, какое-нибудь
usersRange.map([](const auto& user) { return user.getUserName(); })
, которое абсолютно синтаксической заменой дешугарится в структуру с шаблонным оператором-скобочки и становится валидным C++98. Зачем тут вообще Хиндли-Милнер?Не было полвека тому назад никакого Хиндли-Милнера в общедоступных языках.
ML появился в 73-м году (через год после С, ага), так что всего через 5 лет это утверждение станет неверным даже формально.
Как формалист формалисту, я опровергаю другое утверждение.
>Но… «Функции высокого порядка» умели еще Pascal и C, еще порядка полувека тому назад. А возможно и более древние языки.
Мне в этом комментарии не понравилось то, что даже если можно написать на C или паскале частичное применение или композицию функций (мне лень проверять, но вполне допускаю, что можно), все равно в этих языках нет и не было возможности вывести тип той функции, которая получится. И результат в лучшем случае будет небезопасен. То есть, по сравнению даже с современной Java, ее лямбдами, и ее возможностями по созданию функций высших порядков (а они далеко не идеал), в Pascal и C 50 лет назад все было намного хуже.
Если же убрать отсюда вот эти конкретные языки, то эта фраза вообще не вызывала бы никаких вопросов, потому что лисп, и он 1958 года, т.е. старше очень многих. Ну и упомянутый вами ML.
При чем тут Х-М? Исключительно при том, что в дополнение к функциям высших порядков все-таки хочется иметь порою и статическую типизацию, и выведение типов. В вашем примере результат
usersRange.map([](const auto& user) { return user.getUserName(); })
какого типа будет? Нужно ли его будет явно записать, или его нам выведут? Если вы поменяете устройство usersRange, вся эта конструкция в C++ все еще будет работать?
какого типа будет?
Статически известного. Какого — зависит от реализации
map
(как и с Х-М, впрочем). Может, std::vector<std::string>
, может, тоже рейндж какой со строками.Нужно ли его будет явно записать, или его нам выведут?
Напишете
auto
— выведут.Если вы поменяете устройство usersRange, вся эта конструкция в C++ все еще будет работать?
Смотря как поменяю. Если совместимым образом, и если особенно вы нигде явно не требуете никаких типов, а полагаетесь на их вывод, то да.
Я это вполне могу написать на плюсах, какое-нибудь usersRange.map([](const auto& user) { return user.getUserName(); })
И с какого кода вы можете написать auto в плюсах? Как будут работать замыкания?
Зачем тут вообще Хиндли-Милнер?
Автор комментария тут, очевидно, подразумевал любой вывод типов ("Хиндли-Милнер" ведь звучит намного круче, чем просто "вывод типов", правда?). Тип user в рассматриваемом примере компилятор откуда берет?
И с какого кода вы можете написать auto в плюсах?
С 11-го. Но от auto до Х-М всё равно, скажем так, далековато.
Правила вывода, кстати, которые используются auto, уже были сформулированы в плюсах году в 98-м в виде стандарта.
Как будут работать замыкания?
А что с ним?
Тип user в рассматриваемом примере компилятор откуда берет?
Из точки вызова, как в любом другом шаблоне.
Ну напишите вы вместо лямбды
struct NameGetter
{
template<typename T>
std::string operator() (const T& user) const
{
return user.getUserName();
}
};
С 11-го.
Ой, как быстро время-то бежит. По ощущениям казалось, что совсем недавно это и было.
А что с ним?
Ну я если честно не знаю, как в плюсах работают замыкания, т.к. последний раз писал на плюсах еще до того, как они там появились. Как осуществляется захват переменных?
Ну я если честно не знаю, как в плюсах работают замыкания, т.к. последний раз писал на плюсах еще до того, как они там появились. Как осуществляется захват переменных?
Лямбды разворачиваются в анонимную структуру вроде той, что в предыдущем комментарии. Соответственно, захваченные переменные — это просто поля этой структуры. Можно захватывать по значению (и тогда они туда скопируются) или по ссылке (и тогда поле будет иметь ссылочный тип, и сохранится, соответственно, ссылка).
Естественно, никуда без стрельбы по ногам:
auto makeAdder (int a)
{
// Ой, ссылка на локальную переменную, больно
return [&a] (int b) { return a + b; };
}
Лямбды разворачиваются в анонимную структуру вроде той, что в предыдущем комментарии. Соответственно, захваченные переменные — это просто поля этой структуры.
А какой тип у лямбды? Это, получается, пара из указателя на ф-ю и данной структуры?
А какой тип у лямбды?
Какой-то анонимный. Кстати, поэтому до C++14 вы не могли вернуть из функции лямбду, а только лишь type erased-обёртку. Ну или сишный указатель на фукнцию — если у лямбды нет захваченных переменных (со своими особенностями, статики и компилтайм-константы захватывать можно), она к нему приводится.
Предыдущий пример компилятор переписывает примерно как
auto makeAdder (int a)
{
struct
{
int& a;
int operator() (int b) const
{
return a + b;
}
} smth_unnamed { a };
return smth_unnamed;
}
Это, получается, пара из указателя на ф-ю и данной структуры?
Нет, просто некоторая структура-функтор (в плюсовом понимании).
Паскаль и Си очень ограниченно умели возвращать функции из функций (для этого нужны или лямбды, или ещё какая поддержка от языка). Функцию можно было только вызвать и взять указатель на неё, а, к примеру, частичное применение функции сделать — опаньки.
(Хотя, конечно, можно эмулировать)
Глянул краем глаза (смотрел "partial application") — что-то на первый взгляд в книге нет даже очевидных для меня костылей, позволяющих сделать частичное применение (делаем аналог плюсового функционального объекта с чуть менее удобным синтаксисом), зато куча опасного вроде преобразования туда-сюда void*. Но всё равно спасибо за ссылку.
Примечание для Java-программистов: речь не идёт о статической типизации. Некоторые ФП-языки имеют прекрасные системы статических типов, но всё же пользуются преимуществами структурированных типов и/или HKT(higher-kinded types).
Я не очень понял противопоставление в этом предложении. Структурированные типы (а что это такое, кстати? ADT?) и HKT — это вполне себе элементы статической типизации, как и тайпклассы, как и ad-hoc polymorphism.
Структурированные типы (а что это такое, кстати? ADT?)
Очевидно, структурный сабтайпинг (как в TS). TaPL, chapter 15.
Помню как студентами мы делал лабы на ассемблере для MS DOS и аналогов IBM 360 с низкоуровневым доступом к периферийным устройстам. Так вот эта концепция промисов там уже была: при отправке в порты устройства команд, надо было оставить где-нибудь адрес колбека, который вызвался по завершении. Запрограммировать циклы таких операций можно было функциональным подходом и рекурсией, но было жутко неудобно. Наверное поэтому в любых ОС есть более высокоуровневые интерфейсы, при вызове которых процесс может приостанавливаеться до завершения, что позволяет программировать логику «в лоб».
Но ОС проектируют годы, а джаваскрипт за 10 дней.
Вообще забавно наблюдать как хайпят ФП… и прочие сомнительные фичи джаваскриптаА причём тут джаваскрипт?
Так вот эта концепция промисов там уже была: при отправке в порты устройства команд, надо было оставить где-нибудь адрес колбекаА причём тут промисы? Промисы как раз и сделаны, чтобы вручную коллбэки не передавать.
Запрограммировать циклы таких операций можно было функциональным подходом и рекурсией, но было жутко неудобно.Так это и в JS не приходится рекурсией решать.
Наверное поэтому в любых ОС есть более высокоуровневые интерфейсы, при вызове которых процесс может приостанавливаеться до завершения, что позволяет программировать логику «в лоб».А если не нужно, чтобы процесс «спал»? А если нужно отправит 10 запросов, дождаться, пока вернётся хотя бы три, и что-то делать дальше? А если таких задач тысячи, то спавним тысячи процессов?
Я в принципе не против ни одного из этих подходов, в каждом есть что-то полезное. Но вот конкретно с JS уже не первый раз, когда проблемы дизайна языка прикрывают хайпом. Например коллбеки — хорошая вещь, но когда это единственно возможный способ работы с медленными функциями, то это проблема дизайна.
А чтобы не спавнить процессы на каждый из 10 запросов уже десятки лет как существуют функции типа posix select или poll, и по крайней мере есть выбор, спавнить или нет.
Настоящий толчек в массы для ФП дал JS, но не потому что ФП это прямо всегда самый лучший подход, а потому что в JS иначе трудно писать сложную логику с последовательную вызовами IO.
Я не пишу на JS (да и не знаю его толком), но я не могу сказать, что JS дал толчок ФП в массы.
Примерно с тем же успехом толчок ФП в массы дал какой-нибудь питон несколько лет назад, когда некоторые люди внезапно решили, что map, fold и лямбды — это прям квинтэссенция ФП.
Настоящий толчек в массы для ФП дал JS
Ничего подобного. Настоящий толчёк ФП в массы дал некоторый снобистский хайп вокруг Haskell, Erlang и Scala. В JS можно (и иногда нужно) писать в ФП-стиле, но надо понимать, что в половине случаев это не даёт тех потрясающих плюшек, что дают взрослые ФП-языки.
А в массы они не выходили по причине того, что долгое время по-сути и не было проблем, для которых были необходимы ФП-концепции. Как минимум можно передать горячий привет многоядерности и параллелизму.
Но ведь всё, что можно написать в парадигме ФП, можно написать и в ООП, в крайнем случае, наплодив объектов с методом apply(). Более того, жили же на java до введения лямбд.
И наоборот, любой объект можно переписать как функцию.
Не вижу смысла от ФП отказываться, даже в рамках примера. Так можно и от умножения отказаться, и говорить, как плохо. Война с мельницами какая-то.
Но ведь всё, что можно написать в парадигме ФП, можно написать и в ООП, в крайнем случае, наплодив объектов с методом apply(). Более того, жили же на java до введения лямбд.
Но лямбды — это не ФП, как и ФП — не лямбды. ФП — это про чистые функции, алгебру типов и всякое такое. Можно сказать, что плюсовые темплейты ближе к ФП, чем эти несчастные лямбды/мап/фолд в JS, Java и так далее.
Хотя, конечно, любители лиспа и производных со мной не согласятся, но про это есть известная шутка.
И наоборот, любой объект можно переписать как функцию.
Я бы посмотрел на переписывание какого-нибудь god object на чистых функциях с минимумом эффектов.
God object — пример плохо пахнущего кода. Хорошо структурированный код гораздо проще переписать на чистые функции. Вопрос только в том, нужно ли это?
Почему реализации map/fold где-нибудь в js зачастую неполноценны? Потому что ленивости скажем нет, и в итоге где-то в цепочке могут сохраняться промежуточные результаты непонятного размера, просаживая производительность. Почему функции в java не совсем полноценные? Потому что чистоту мы не можем ни обеспечить, ни проверить, нет механизмов для этого.
Но это все не значит, что мы не можем думать о программах на js или java таким же способом, как делали бы это в идеальном ФП языке. И мне кажется, что ФП это больше про способ думать о коде, о композиции его из частей, о том, какие это должны быть части. И «эти несчастные лямбды/мап/фолд в JS, Java и так далее.» — ничто иное, как именно способ думать о коде иначе. Не иначе чем в плюсовых темплейтах, а иначе чем мы делали в этом же языке ранее.
Вопрос в том, о каком коде вы думаете и как именно вы думаете.
Когда я думаю о своём коде, я думаю в рамках типов. Нередко самое большое усилие для меня — написать сигнатуры, оставив undefined где надо, и потом тупо заполнять дырки, почти выключив мозг, чтобы оно тайпчекалось. Ну или generate definition, case split, make case, case split, obvious proof search, и в продакшен. А мапы-лямбды — так, инструмент. Можно и без них. В конце концов, вы же даже map id = id доказать не сможете, а в хаскеле это и не выполняется, потому что Hask is not a category.
Когда я думаю о чужом коде (или о своём через год), я смотрю на типы. Я не доверяю автору и его размышлениям о мапах-фолдах и чистоте, мне надо, чтобы компилятор мне что-то интересное рассказал, живёт ли эта функция в IO, нет ли там костылей вроде
if (typeof(param) == "Kludgy") doKludge();
, и так далее.Функциональное программирование — это парадигма использования чистых функций в качестве неделимых единиц композиции, с избеганием общего изменяемого состояния и побочных эффектов.Функциональное программирование не просто избегает общего изменяемого состояния, а не содержит никакого изменяемого состояния — ни общего, ни частного. Если же, все-таки, изменяемое состояние есть, то речь идёт о смеси функционального и императивного прогаммирования.
Может определение и любимое, но оно, как минимум, неточное и ведёт к неверным выводам.
Если же учесть, что функциональная и императивная парадигмы на базовом уровне — это вопрос интерпретации, то можно прийти к заключению, что статья является упражнением в софистике. Ведь если идти до конца, то любую императивную программу можно интерпретировать как функциональную и запрет на функциональное программирование будет обозначать запрет на программирование вообще.
Но обычно имеют ввиду не это.
А программировать надо так, как навязывает язык и его расширения-библиотеки, иначе всё время придётся бороться с ветряными мельницами. Только космических выводов из этого делать, пожалуй, не стоит. Большинство ЯВУ, начиная с Фортрана, были синтетическими.
Если же, все-таки, изменяемое состояние есть, то речь идёт о смеси функционального и императивного прогаммирования.
В правильном™ ФП нет никакого изменяемого состояния, и даже все эти монады
ST
и IO
— это просто возможность программисту оперировать чистыми функциями для построения чистой композиции этих самых функций, оперирующих нечистым миром (или нечистым состоянием) при помощи рантайма. В конце концов, IO
— это всего лишь State
, тегированный #RealWorld
.Но да, вы абсолютно правы, на базовом уровне это вопрос интерпретации. В конце концов, можно написать библиотечку
IO
-функций таких, что переписывание кода с произвольных плюсов или, не знаю, ассемблера будет по большей части тупым синтаксическим преобразованием.Хороший язык программирования помогает программистам писать хорошие программы. Ни один из языков программирования не может запретить своим пользователям писать плохие программы.
(идет за чипсами и колой)
Давайте рефакторим код так, чтобы он больше не относился к ФП. Можно сделать класс с публичным свойством. Поскольку инкапсуляция не используется, было бы преувеличением назвать это ООП.
И в очередной раз инкапсуляцию связывают со способностью языка явным образом помечать поля класса как «private». Не об этом же инкапсуляция!
В вашем примере:
class User {
constructor ({name}) {
this.name = name;
}
...
}
name — вполне себе инкапсулированное поле.
Существует мнение (я его не разделяю), что инкапсуляция это и есть сокрытие.
https://ru.wikipedia.org/wiki/Инкапсуляция_(программирование)
приводит к другому распространённому заблуждению — рассмотрению инкапсуляции неотрывно от сокрытия. В частности, в сообществе С++ или Java принято рассматривать инкапсуляцию без сокрытия как неполноценную. Однако, некоторые языки (например, Smalltalk, Python) реализуют инкапсуляцию в полной мере, но не предусматривают возможности скрытия в принципе.
const T& getFoo() const;
T& setFoo();
Бессмысленно чуть более чем. Разве что, факт обращений к полю логгировать.
«Но это же разные типы. Конечно, вы не можете применять метод класса person к стране!».
На это я отвечу: «А почему нет?»
Действительно, почему нет?
Узнайте имя у страны, потом фамилию и паспортные данные.
Смело двигайтесь дальше — извлекайте корни из любого поля любого объекта. Если у какого-то объекта корень из ip-адреса не извлекается, то это его проблемы.
Зато строчек кода мало!
У автора единственная реализация getName, и утверждается, что она может работать с любым объектом. Это не так (если ожидать корректную работу функции и наличие некоторого смысла).
У вас же описан частный случай реализации для определенного объекта, и я не понимаю, почему вы мне возражаете.
На scala это через рефлексию вызывается, обычно лучше всё-таки сгородить trait.
Стандартная проблема фпшника: на яблоках и пальцах всё красиво считается и другим доказывается, а в реальных задачах без монад, переусложнений, кровавых соплей и неразумной траты ресурсов концы не сходятся.
Мне кажется, что дело не в ФП. Не скажу за react, но в angular используются реактивное программирование, что значительно отличается от функционального. И самый большой минус, что используется далеко не во всем коде, поэтому следить за тем, что observable, а что нет и какие изменения к чему приводят новичкам (например, мне) бывает сложно.
Всё отлично когда «чисто».
Как только смешивают в одном приложении (то есть коде) ФП, ООП, реактивное — то надо, похоже, смотреть в оба и не допустить «мути» — то есть «взбалтывания» всех этих парадигм в одном «флаконе» — что на практике постоянно и происходит ибо это уже практика — то есть «жизнь кода» вне академических «песочниц».
А разве это не так? :) Честно говоря, я не вижу ничего плохого в замене циклов на map-reduce, при одном простом условии — что это делается осознанно, понимая, что мы приобретаем, и чем за это платим. При этом, заметьте, человек, способный проделать такую замену, уже не ограничен javascript и фронтендом, ему можно завтра дать в руки Hadoop — и он не потеряется там совсем.
Что мешает объявить интерфейс IName и реализовать у классов для которых это имеет смысл?
const getName = obj => obj.name;
const name = getName({ uid: '123', name1: 'Banksy' });
Собственно, поэтому не имеет особого смысла рассматривать всю эту функциональную ерунду в отрыве от типизации, которая обеспечивает лютую долю удобства. Зачем вам чистые функции, если вы их чистоту можете гарантировать только пристальным вглядыванием?
Функциональное программирование — это парадигма использования чистых функций в качестве неделимых единиц композиции, с избеганием общего изменяемого состояния и побочных эффектов.
Чистая функция:
Получая одни и те же входные данные, всегда возвращает один и тот же результат
Как быть с random? Эта функция получает одинаковые данные (интервал чисел), но всегда возвращает случайное число из заданного интервала. Получается она не чистая и не может быть использована в ФП по определению выше.
Нет. Это, если хотите, часть соглашения о чистых функциях: есть некое внешнее хранилище состояний World, к которому можно обращаться только через монаду IO. Если сильно упростить — то функция, для работы которой монада IO не нужна, которая сама по себе ничего во внешнем мире изменить не способна — считается чистой, и ее можно лениво выполнять, безопасно параллелить итд. Как видите, тут «внешний мир» — хорошая, годная, даже необходимая абстракция.
> считаю радикалов, которые не хотят признать, что ни ФП, ни ООП, ни молоток не являются наилучшими инструментами
А такие вообще встречаются в дикой природе?
Просто это правда удобно, когда вы можете посмотреть на один лишь тип функции и сразу сделать выводы, что она не полезет на сервер, не начнёт читать файлы, не сохранит что-то в БД, не будет использовать источники недетерминированности для своего результата.
Ну и внезапно оказывается, что и тестировать такой код сильно легче, например.
Да, научиться программировать на ФП с монадками и этим всем действительно требует больших усилий, у них крутая кривая обучения. Но после того, как вы научились, мозг можно включать всё реже, и это приятно. А что мозг практически не нужно включать для отладки, потому что отладка нужна очень нечасто — приятно вдвойне.
Не рассказывайте только авторам таких статей про зависимые типы, они ж совсем изойдут.
Только правильно к нему относиться как «вот так тоже можно сделать», а не «только так и нужно делать». Есть, в конце концов, такая очень забавная статья, но это не значит, что для обработки регулярок на хаскеле вам нужно городить линейную алгебру.
Получается она не чистаяОна не чистая. Да и любая функция ввода/вывода или обращения к базе или читающая файл или пишущая в файл — не чистая.
Где вы видели random, возвращающий именно случайное число?
Хорошо, пусть это псевдослучайное число или даже другая функция, которая при каждом вызове возвращает следующее значение некой последовательности чисел (вспоминаем python, его yield, итераторы и генераторы). Передавая одни и те же входные данные, будем получать разные результаты. Правильно ли я понял, эти функции не чистые по определению выше и не могут быть использованы в ФП?
P.S. Немного промахнулся в ответе на сообщение s-kozlov
вспоминаем python, его yield, итераторы и генераторы
Правильный аналог yield, итераторов и генераторов в ФП — это (бесконечные ленивые) списки, по большому счёту. И тогда функция, принимающая сид и производящая таковой список, вполне себе будет чистой.
Собственно, если к спискам относиться не как к структуре данных, а как к управляющей структуре, то всё встаёт на свои места.
Предыдущая функция getName() работала с любым входящим объектом.
Такую функцию в указанном виде можно написать только при наличии структурного сабтайпинга (если не учитывать динамику). При чем тут функциональное программирование, если 99% функциональных языков такового не имеют?
Промисы — это монады.
На практике промисы и async/await НЕ реализованы как монады.
У автора каша в голове, смешались вместе кони, люди и все остальное..
Можно ли осознанно отказаться от функционального программирования?