Pull to refresh

Comments 101

«Но почему я не могу продолжать использовать ООП?»
Мне тоже интересно.
Мне кажется автор статьи ответил на этот вопрос.
«Придётся приложить достаточно усилий, чтобы корректно обновлять и синхронизировать все потоки.»
Автор оригинальной статьи не умеет в синхронизацию в ООП, поэтому просит всех переходить на ФП?
В статье он прямым текстом пишет «я не агитирую», мне кажется основная цель показать красоту ФП :)
UFO just landed and posted this here
Понятно, что вы привыкли к if/else, только в мире не только едят котлеты с картошкой, есть и миллионы других блюд. Не кажется ли вам, что такая «узколобость» только ограничивает вас? Скажу вам больше, в ФП вообще нет for/while только рекурсии. И мне они кажутся очень удобными.
UFO just landed and posted this here
Про Йоду-гастарбайтера — это вы про синтаксис или про текст в кавычках? Потому как синтаксис примера с guess почти один в один switch-case:
switch guess{
 case 7: return "Much 7 very wow."; break;
}
Или так, через подобие тернарного оператора:
(guess == 7)?return ("Much 7 very wow.")
Только более компактно.
«каждый своего жука хвалит» (с) «Том Сойер» если не ошибаюсь
вообще доверять иностранным авторам только потому что они написали книгу или статью. Попадались мне книги, дословно не помню но примерное содержание фраз:
-в байте 9 бит, но при поступлении в процессор один бит теряется.
-регистры IDE как-то хитро сделаны, по 0 смещению идет работа с 16 битным словом, а по смещению 1 доступен по чтению 8 битный регистр ошибок, как они это сделали? какой-то радиолюбительский трюк.
Простите, а кто-то умеет? Тут, на мой взгляд, как повезет. Сумел предусмотреть все кейсы — молодец. Нет — получай дедлоки или состояние гонки. И хуже всего, когда автор кода слишком уверен в своих способностях мыслить в терминах нескольких потоков одновременно.
В любом современном императивном языке состояние по умолчанию не разделяемое и явная работа с разделяемым состоянием является антипаттерном.
> В любом современном императивном языке состояние по умолчанию не разделяемое

В каком, например? C, Java, C#, Python?
D, Go, Rust.

С, Java, C# — языки с устаревшим дизайном.

В Pyhon многопоточности фактически нет, ибо GIL.
Путаете квантор существования и квантор всеобщности?
Не путаю. Прямая работа с разделяемым состоянием в многопоточной среде сейчас считается дурной практикой по всем известным причинам. Во времена появления Java, C# и, прости господи, C это было ещё не очевидно. Поэтому все современные языки по умолчанию состояние не разделяют. А те не многие поделки, что таки разделяют современными считаться не могут, так как отстали в этом плане от развития компьютерной науки.
Хорошо. Если взять за основу ваше понимание слова «современный», то соглашусь :)
Я вам даже ещё процитирую Александреску:

Основные императивные языки наших дней (такие как C, C++, Java) развивались в век классической многопоточности – в старые добрые времена простых архитектур памяти, понятных примитивов взаимоблокировки и разделения данных без изысков. Языки, естественно, моделировали реалии аппаратного обеспечения того времени (когда подразумевалось, что потоки разделяют одну и ту же область памяти) и включали соответствующие средства. В конце концов само определение многопоточности подразумевает, что все потоки, в отличие от процессов операционной системы, разделяют одно общее адресное пространство.

Кроме того, API для реализации обмена сообщениями (например, спецификация MPI) были доступны лишь в форме библиотек, изначально созданных для специализированного дорогостоящего аппаратного обеспечения, такого как кластеры (супер)компьютеров.

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

Наконец был разработан язык Erlang. Он начал свой путь в конце 1980-х как предметно-ориентированный встроенный язык приложений для телефонии. Предметная область, предполагая десятки тысяч программ, одновременно запущенных на одной машине, заставляла отдать предпочтение обмену сообщениями, когда информация передается в стиле «выстрелил – забыл». Аппаратное обеспечение и операционные системы по большей части не были оптимизированы для таких нагрузок, но Erlang изначально запускался на специализированной платформе. В результате получился язык, оригинальным образом сочетающий нечистый функциональный стиль, серьезные возможности для параллельных вычислений и стойкое предпочтение обмена сообщениями (никакого разделения памяти!).

Перенесемся в 2010-е. Сегодня даже у средних машин больше одного процессора, а главная задача десятилетия – уместить на кристалле как можно больше ЦПУ. Отсюда и последствия, самое важное из которых – конец монолитной разделяемой памяти.

С одним разделяемым по времени ЦПУ связана одна подсистема памяти – с буферами, несколькими уровнями кэшей, все по полной программе. Независимо от того, как ЦПУ управляет разделением времени, чтение и запись проходят по одному и тому же маршруту, а потому видение памяти у разных потоков остается когерентным. Несколько взаимосвязанных ЦПУ, напротив, не могут позволить себе разделять подсистему кэша: такой кэш потребовал бы мультипортового доступа (что дорого и слабо масштабируемо), и его было бы трудно разместить в непосредственной близости ко всем ЦПУ сразу. Вот почему практически все современные ЦПУ производятся со своей кэш-памятью, предназначенной лишь для их собственных нужд. Производительность мультипроцессорной системы зависит главным образом от аппаратного обеспечения и протоколов, соединяющих комплексы ЦПУ+кэш.

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

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

Протоколы синхронизации кэшей добавляют к сюжету еще один поворот (хотя и без него все было достаточно лихо закручено): они воспринимают данные только блоками, не предусматривая чтение и запись отдельных слов. То есть общающиеся друг с другом процессы «не помнят» точный порядок, в котором записывались данные, что приводит к парадоксальному поведению, которое не поддается разумному объяснению и противоречит здравому смыслу: один поток записывает x, а затем y, и в некоторый промежуток времени другой поток видит новое y, но старое x. Такие нарушения причинно-следственных связей слабо вписываются в общую модель классической многопоточности. Даже наиболее сведущим в классической многопоточности программистам невероятно трудно адаптировать свой стиль и шаблоны программирования к новым архитектурам памяти.
функциональное и императивное существуют в разное время, совместить их нельзя, любая программа имеет функциональную и объектную часть. Первая отвечает за поток управления, вторая за состояние.

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

Но за бортом осталось описание «входного-выходного» мира… тут-то и используют объекты/состояния. Разница в программировании тут существенна: 1) ООП в императивном стиле — нарезать мир на плоскости последовательности состояний, и подавать их последовательно на вход простого преобразователя (вашей программы) состояний; 2) ООП в функциональном стиле — нарезается мир на последовательность преобразований, которые и применяются последовательно друг к другу, а потом этот клубок исполняется на входном состоянии (если бы в haskell был бы параллельный исполнитель, то можно было бы натравливать на входное состояние много ядер [что-то вроде fork-map-reduce], и си код стал бы тормазнутым из-за немасштабируемости). Как недостаток функционального программирования можно назвать — нелокальность представления, которое необходимо костылить декомпозицией и синтаксическим сахором.

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

Вывод: для получения выгоды от применения этих парадигм нужны специфические задачи и условия (поддержка языка и архитектуры) и синергия из-за этого не получится, но самое противное — это разное мышление и программируя на haskell в слиле си, или на плюсах/java/scala в стиле haskell получим недостатки обеих стилей, с одним плюсом — достаточно знать один из языков.
Присоединяюсь, почему это нельзя использовать ООП вместе с ФП, раз уж эти парадигмы описывают разные вещи?
Как вы можете использовать ложку, если познали существование вилки?! Еретик!
Автор делает вид, что параллельное программирование появилось только сейчас. И упорно не замечает, что всё, что он пытается приписать исключительно функциональным языкам, имеет давно отработанные механизмы в императивных языках.

Да, некоторые вещи в функциональных языках можно записать короче. Ну и что? На языке APL сложнейшие вычисления можно записать одной строкой — стало-ли от этого кому-то легче?
Всегда удивляли авторы статей, у которых ошибки в пунктуации чуть ли не в каждом предложении.
Вы пишете так, чтобы побесить читателей? Я понимаю, что бывают опечатки, но просто безграмотно написанная и абсолютно не проверенная статья — это уже перебор. Обычно я игнорирую такие вещи, но сейчас уже просто переполнилась чаша моего терпения… нельзя же так. Если Вы пишете для себя — пишите в уголке и не выставляйте на всеобщее обозрение. Если для людей — будьте добры, выпускайте качественный продукт.
Уговорили, переводить и писать больше не буду.
Не обращайте внимания. Есть масса людей, для которых форма гораздо важнее содержания. Вот как поступил бы здравый человек? Написал в личку где именно много ошибок. Помог исправить. Как поступают эээ… неразумные? Заявляют всему миру, что вот как раз они-то — самые умные и грамотные. И смысл без запятой уловить не в состоянии. Простите, ощущаютъ некое амбрэ. _вытер носик платочком_

Хотите чему-то научиться, да ещё бесплатно? Учитесь, говорите спасибо. Хотите все запятые — покупайте книги профессионально проверенные редакторами. И идите в суды если что не так.

К сожалению мы не все обладаем 100% грамотностью. Мало того, не для всех русский язык родной. Мало того, даже для русскоязычных, не для всех русский язык основной. Но люди прикладывают массу усилий (как вы, например), чтобы поделиться с другими опытом. СПАСИБО.

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

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

Статьи бывают полезные и бесполезные. Полезным прощается всё. Бесполезные (как статьи так и комментарии) и с запятыми никому не нужны.
Будьте добры, не решайте за всех людей. Мне вот статья понравилась и недостаток запятых я переживу. У нас не так много авторов занимаются переводом интересных статей, больше новости постят и рекламу.
Если интересно, могу добавить примеры на Erlang. Правда не уверен, поддерживается ли его подсветка тут.
Конечно, интересно:)
Кстати, было бы здорово если бы примеры-пояснения из ООП были не только на JS, но и, например, на C#.
Вы, вероятно, не в курсе, что обычно люди, которым ошибки режут глаза не стесняются писать в PM с указанием списка оных? А ежели лень — то и глаза, выходит, не режет.
Я очень надеюсь, что автор, кроме изучения функционального программирования, немного обратит еще внимания и на русский язык. А расписывать ошибки, коих более чем достаточно — это уже немного другая тема и обсуждается на других сайтах. Да, я в курсе, что отдельные ошибки пишут личкой, но не в таком количестве. И спасибо за слив кармы и кучи минусов, но надеюсь это поможет стать гигтаймсу хоть немножеско грамотнее, кто-то же должен об этом говорить.
UFO just landed and posted this here
Обмен сообщениями тоже вещь не бесплатная, в идеале было бы круто, чтобы для работы их как можно меньше требовалось.
UFO just landed and posted this here
А это походу бесполезно объяснять адептам ООП.
У них есть железобетонный аргумент «я то же самое могу написать на ООП».
Им нет дела, до более корректных инженерных решений.
Они не понимают, что такое надежность, предсказуемость и т.д.
Лучше всего про это сказал кто-то из докладчиков, вроде на джипоинте.
Правда он говорил это про тесты, но суть можно применить ко многим вещам.

«Чак Норрис не ходит на охоту, потому что охота подразумевает неудачу. Чак Норрис ходит убивать.»

Так и инженер, может писать программу без тестов и тогда он «ходит на охоту».
Или писать программы сложным подходом с кучей потенциальных проблем, но который он уже применял.
И делать это просто потому, что он не знает/не понимает как можно делать лучше, проще и с большей гарантией.
Я с функциональным программированием сталкивался лишь в подобных вводных статьях, но мне, как человеку со стороны, такое программирование кажется лишь шагом назад в череде абстракций, наложенных друг на друга с развитием языков программирования. Ведь люди (и программисты, я уверен, тоже) не мыслят рекурсиями и подобными функциональными выкрутасами. Всякие if..else, while и for схожи с людским типом мышления. Ещё больше с ним схожи выражения в духе
for (нога : сороконожка) {
нога.поднять();
}

Такие конструкции ведь писать и понимать совсем не сложно и думать нужно над тем «что именно нужно сделать?», а не «как вообще это делается?».
А как по мне такой код еще более читаемый — сороконожка.форич(нога=>нога.поднять());
Наверное, такими вопросами должна заниматься какая-нибудь профильная лингвистика. Мне вот совсем не кажется логичным то, что «форич» вызывается у объекта-сороконожки. Но это частности :)
Я к тому, что «многопоточность» и прочие клёвые слова — это здорово, но код-таки должно становиться писать легче, а не тяжелее. И нельзя сказать, что «вы просто не привыкли к такому типу программирования, потому тяжело» — такие функциональные выкрутасы тяжелы из-за несоответствия им нашего мышления. Неспроста ведь выделяют глаголы (функции) и существительные (объекты). Правда инфинитивы — это скорее существительные, а не глаголы, что скажет вам филолог, проходивший историю языка, а в английском там вообще всё ещё страннее и чудоковатее
Ваш код не имеет никакого отношения к ФП. Вы передали грязную функцию первого порядка в грязную функцию высшего порядка.

В ФП этот код выглядел бы так:
сороконожка_с_поднятыми_ногами = сороконожка.мап( нога => нога.поднять() )
Ну я же правильно понимаю, на выходе будет две сороканожки? Когда сороканожка — это реальный робот с сорока ногами, получится немного не то.
В этом вся суть функционального программирования — вы создаёте новый объект с новыми свойствами, а старый просто выбрасываете. :-)
Совсем не так :-) В функциональном — Вы создаете новое вычисление, которое что-то вычисляет, а старое просто выбрасываете :-))
Новое вычисление вычисляет её более новое вычисление :-)

Все проще :)


сороконожка.форичМ(нога=>нога.поднять());

(Здесь М означает монаду)

Еще бы таймер на опускание выставили, а то ж плюхнется сороконожка
поднимание_ног [] = []
поднимание_ног(нога:ноги) = поднять(нога) : поднимание_ног[ноги]

поднимание_ног(сороконожка)

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

Потратил 10 секунд на python, столько же на ruby. На C/C++/Java/etc. это получится чуть больше.
>>> def plus1(lst):
...     return [x+1 for x in lst]
... 
>>> plus1([1,2,3])
[2, 3, 4]
>>> 

irb> def plus1(lst)
irb>   lst.map {|x, obj| x+1}
irb> end
=> :plus1
irb> plus1([1,2,3])
=> [2, 3, 4]
А разве генераторы списков и map это не функциональщина?
Если я понял правильно автора статьи и вообще концепцию ФП, то надо сказать «нет» императивному подходу.
Я просто показал, что в языках с императивным стилем можно создавать вполне лаконичные конструкции-аналоги.
Согласен, мои примеры не совсем императивные, но это то, что используют «императивщики» каждый день.
Я предпочитаю балансировать и не вдаваться в крайности. Я не вижу смысла переходить на Erlang только потому, что это, возможно, круто.

Я вот к стати попробовал на С++14(уже 300 лет не прикасался к С++ выше С++98):
template<typename T>
auto plus1(const vector<T> &lst)
{
    auto mod = lst;
    transform(mod.begin(), mod.end(), mod.begin(),
        [](auto &x) {
            return x+1;
        });
    return mod;
}

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

templateT plus1(const vector &lst)
{
T result;
for(T::const_iterator it = lst.begin(); it != lst.end(); ++it)
result.push_back(it.value() + 1);
return result;
}
Вообще я так посмотрел и можно сделать так(раз уж идет копирование):
template<typename T>
auto plus1(const vector<T> lst)
{
    transform(lst.begin(), lst.end(), lst.begin(), [](auto &x) { return x+1; });
    return lst;
}

Конечно, если T не PDO, то будет неэффективно.
UFO just landed and posted this here
public static int[] incrementArray(int... array) {
    for(int i = 0; i < array.length; array[i] += 1, i++) {}
    return array;
}

Думаю, можно не считать закрывающую скобку как строку?
На правах некропоста напишу ещё восьмую джаву:
public static int[] incrementArray(int... array) {
    return Arrays.stream(array).map(num -> num++).toArray();
}
Потому что проблемы ООП vs ФП не существует? =)
guess x = "Ooops, try again."

x и не нужен:

guess _ = "Ooops, try again."


Тако ж

plus1 []      = []
plus1 (x:xs)  = x + 1 : plus1 xs 

суть лишь

plus1 = map (+1)
Я просто оставлю вам одну главу из книги «Язык программирования D» от Андрея Александреску:

5.11.1.1. «Чист тот, кто чисто поступает»

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

Посмотрим, как работает это допущение. В качестве примера возьмем наивную реализацию функции Фибоначчи в функциональном стиле:

ulong fib(uint n) {
    return n < 2 ? n : fib(n - 1) + fib(n - 2);
}

Ни один преподаватель программирования никогда не должен учить реализовывать расчет чисел Фибоначчи таким способом. Чтобы вычислить результат, функции fib требуется экспоненциальное время, поэтому все, чему она может научить, – это пренебрежение сложностью и ценой вычислений, лозунг «небрежно, зато находчиво» и спортивный стиль вождения. Хотите знать, чем плох экспоненциальный порядок? Вызовы fib(10) и fib(20) на современной машине не займут много времени, но вызов fib(50) обрабатывается уже 19 минут. Вполне вероятно, что вычисление fib(1000) переживет человечество.

Хорошо, но как выглядит «правильная» функциональная реализация Фибоначчи?

ulong fib(uint n) {
    ulong iter(uint i, ulong fib_1, ulong fib_2) {
        return i == n
        ? fib_2
        : iter(i + 1, fib_1 + fib_2, fib_1);
    }
    return iter(0, 1, 0);
}

Переработанная версия вычисляет fib(50) практически мгновенно. Эта реализация требует для выполнения O(n) времени, поскольку оптимизация хвостовой рекурсии (см. раздел 1.4.2) позволяет уменьшить сложность вычислений. (Стоит отметить, что для расчета чисел Фибоначчи существуют и алгоритмы с временем выполнения O(log n)).

Проблема в том, что новая функция fib как бы утратила былое великолепие. Особенность переработанной реализации – две переменные состояния, маскирующиеся под параметры функции, и вполне можно было с чистой совестью написать явный цикл, который зачем-то был закамуфлирован функцией iter:

ulong fib(uint n) {
    ulong fib_1 = 1, fib_2 = 0;
    foreach (i; 0 .. n) {
        auto t = fib_1;
        fib_1 += fib_2;
        fib_2 = t;
    }
    return fib_2;
}

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

Но подумав немного, мы увидим, что итеративная функция fib не такая уж чумазая. Если принять ее за черный ящик, то можно заметить, что при одних и тех же аргументах функция fib всегда возвращает один
и тот же результат, а ведь «красив тот, кто красиво поступает». Тот факт, что она использует локальное изменение состояния, делает ее менее функциональной по букве, но не по духу. Продолжая эту мысль, приходим к очень интересному выводу: пока изменяемое состояние внутри функции остается полностью временным (то есть хранит данные в стеке) и локальным (то есть не передается по ссылке другим функциям, которые могут его нарушить), эту функцию можно считать чистой.

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

pure ulong fib(uint n) {
    ... // Итеративная реализация
}

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

fib n = fibs !! n
        where fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

Красота же :)
Что-то я не понимаю, что ваш код делает :-)
Оператор!!! возвращает элемент списка по его номеру (в данном случает n-й элемент). Список fibs — бесконечный список чисел фибоначчи, который мы определяем следующим образом. Первые два элемента — это 0 и 1 (тут должно быть очевидно), а все последующие элементы получаются комбинированием (а именно, сложением; комбинирование осуществляет функция zipWith с помощью своего первого аргумента) самого списка fibs (т.е. 0,1,...) и его хвоста (tail; хвост списка получается откусыванием первого элемента, головы; т.е. 1,...).

Такое проходит только потому, что хаскелль — ленивый язык, и он не вычисляет весь список целиком (если не предпринимать дополнительных телодвижений).
Эм… zipWith складывает только последние элементы переданных ей списков?
zipWith комбинирует все соответсвующие элементы
> zipWith (+) [1,2,3] [4,5,6]
[5,7,9]


Поскольку fibs — бесконечный список, (tail fibs) — тоже бесконечный список. Вот мы и комбинируме два бесконечных списка поэлементно.
А, понял, лихо закручено :-)
var add = function(a, b){
  return a + b
}

Вы только что создали анонимную функцию, которая получает a и b и возвращает a + b в переменную add.


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

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

Что касается простоты — поправьте, если я не прав, но мне как человеку довольно поверхностно знакомому с функциональными языками кажется, что современные языки типа эрланга, хаскеля добавляют какие-то невероятные объемы сложности туда, где они совершенно не требуются.
Хотя не могу поспорить с тем, что есть некоторые вполне конкретные применения (типа каких-то супер-многопоточных сервисов, способных держать хреналион одновременных коннектов), где тот же эрланг может подойти гораздо лучше других решений. Но это опять же — относится к конкретной экосистеме, а не к ФП в целом.
ответ на вторую часть вопроса… не верно! На функциональных языках можно писать такие же простые хелоуворлды, как и на императивных, но там, где в императивной программе обработки ошибок, управление потоками, распределение и поддержание целосности данных, в функциональной программе — тот же ад и магия 993 уровня на межгалактическом подпространственном кластере. Функциональное программирование никак не мешает писать программы работающие с состояниями, нет нормального функционального языка, который бы мог оптимизировать такие программы (одни со сложным синтаксисом, другие,… плохо оптимизируют), но это недостаток не «функционального программирования» вообще.
> Для меня несколько странно звучит, когда противопоставляют именно ООП и ФП, а не императивщину и функциональщину.

Почему я противопоставил ООП и ФП в контексте описания неидеального мира? Потому что в популярных ООП-языках (Java, C#, C++ и т.д.) реализация ООП завязана на императивную парадигму: объекты имеют изменяемое состояние. В этих языках ООП призвано улучшать модульность и возможности повторного использования императивного кода. Но зачастую ФП позволяет достичь этого более простым путем. Неизменяемость состояния и отсутствие побочных эффектов против инкапсуляции, наследования и полиморфизма. Разумные ограничения против богатых возможностей, которыми еще надо научиться пользоваться (паттерны, SOLID и т.п.) Противопоставлял я их именно в этом смысле, а не вообще. А вообще, сам я в пишу на Scala, где ФП и ООП вполне успешно сочитаются.

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

1. Кроме эрланга и хаскеля, бывают и другие современные ФП-языки.

2. Я считаю, что смысл ФП в упрощении кода, а не в усложнении. Два главных ограничения, неизменяемость состояния и отсутствие побочных эффектов, дают referential transparency — важное свойство, позволяющее достаточно просто проверить корректность программы. Чистые функции позволяют использовать функциональную композицию — мощное средство обеспечения модульности и возможностей повторного использования.

3. Таки да, есть экстремисты от ФП, которые используют его везде и для всего. В том числе, для тех задач, для которых оно не очень подходит (никто и не утверждает, что оно подходит всегда и везде). Скорее всего представление о «невероятных объемах сложности, где они совершенно не требуются» возникло у вас после ознакомления с их творчеством. В этом нет ничего плохого, и это совсем не обязательно. Использовать элементы ФП можно во многих языках, лишь единицы из них заставляют вас писать полностью «чистый» код.
Может кто-то из гуру ФП рассказать, как прогнозировать время работы программы на функциональном языке? То есть можно ли в общем случае глядя на код двух разных программ понять, какая из них будет работать быстрее?

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

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

Проблему вижу в накладных расходах на всю эту магию. Скажем, ООП тоже совсем не бесплатное.

И в целом есть плохие способы писать код на императивном языке (в плане быстродействия), есть хорошие, и можно объяснить чем одни отличаются от других, на что тратятся циклы процессора. В случае ФП я не понимаю как отличить быстрый код от медленного и возможно ли это в принципе, чем руководствуются программисты на ФП при выборе способа написания программы и.т.д. Просто у меня из статьи сложилось впечатление (возможно обманчивое), что предлагается откинуться на спинку стула и не думать о быстродействии, потому что современные компьютеры такие быстрые-пребыстрые.
> Вы сейчас описываете использование готовых алгоритмов из библиотек, я прав? В таком случае конечно не имеет значения на чём там код написан.

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

> Проблему вижу в накладных расходах на всю эту магию. Скажем, ООП тоже совсем не бесплатное.

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

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

Ну там имелось ввиду, что императивный стиль хорошо мапится на фон-неймановскую архитектуру, а функциональный — не очень хорошо. Нагрузку по маппингу в основном на себя берут компиляторы и функциональные структуры данных (из готовых библиотек). И то и другое, как правило, имеет повышенные требования к ресурсам по сравнению с императивными аналогами. С развитием технологий плюсы от использования ФП перевесили минусы от накладных расходов (вполне изученных и достаточно широко известных) на его использование.
Извиняюсь за настойчивость, но я так и не получил ответа на вопрос, как именно программист на функциональном языке выбирает как ему написать эффективную программу? Неужели ответ — швырять любой, логически правильный код в очень умный компилятор и ждать что он сгенерирует оптимальный машинный код?
Вопрос вроде был про быстродействие, а не про эффективность. И ответ на этот вопрос вроде бы уже прозвучал, и не раз. Может я не понимаю, чего вы от меня добиваетесь? Можете сформулировать ответ на свой вопрос для императивного языка? Например для Java или PHP? А я тогда на базе этого ответа попытаюсь выявить различия с функциональным подходом, если они есть.
Хорошо, давайте. Например, при написании программы на языке с ООП мы знаем, что есть затраты на вызов виртуальных методов. Поэтому не очень хорошая идея засовывать тысячи объекты в контейнер и проходить по ним циклом с вызовом виртуальных методов, а ля

for ( baseObj: array )
{
baseObj->foo();
}

В данном случае лучше переписать эту часть, выделив функцию foo из класса, чтобы она работала сразу с коллекцией объектов.
foo( array );
Таким образом мы избегаем ненужных трат на поддержку ООП, хотя в общем-то и в первом случае код выглядит логичным и «правильным» (просто будет более медленным).

Существуют ли подобные правила для написания эффективного кода на функциональных языках?
Существуют. Но как и в вашем примере (некоторые языки не очень-то разделяют виртуальные и невиртуальные функции), правила во многом применимы только к конкретному языку. Например, для Haskell обычным советом (настоятельной рекомендацией) является не изобретать велосипед и не писать явную рекурсию самому, а воспользоваться функциями наподобие fold(l/r)/map/filter и т.п. В Erlang же наоборот, чаще гораздо лучше воспользоваться list comprehension, чем lists:map, потому что для первого компилятор генерирует гораздо более эффективный код. Опять же, из рекомендаций для Haskell, в структурах данных делать поля строгими (чтобы не копить thunk-и), но так как распространённых ленивых языков, кроме Haskell нет, то эту рекомендацию трудно представить применимой для каких-либо ещё функциональных языков.

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

Можно ещё вспомнить о «красивых» рекурсивных решениях (вроде стандартного вычисления чисел Фибоначчи), которые в реальном мире и на реальном железе работают не очень.

Соответственно о стоимости ФП тоже очевидно придётся думать при программировании. Только поскольку я не знаком с ФП, я совершенно не знаю как принимаются подобные решения.
Наверняка для вас не является секретом, что современные компиляторы применяют over 100500 оптимизаций к тому, что накалякал программист. Например, в данном случае, в зависимости от того, как объявлен foo() и что именно из baseObj он использует, компилятор может просто заинлайнить реализацию foo() внутрь цикла. И код не надо менять, и никакого вызова виртуального метода.

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

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

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

Но в то, что программист может обитать в прекрасном мире абстрактных парадигм а компилятор за него думает об оптимизации я не верю.
Последний абзац, возможно, получился несколько резковатым. Но кода я вижу, что в качестве best practices императивного ОО-программирования преподносится призыв заранее уродовать код, без бенчмарков, без поиска реальных узких мест в процессе выполнения — я испытываю горечь :)
Не проще ли специфицировать массив соответствующим типом?
UFO just landed and posted this here
UFO just landed and posted this here
Каррированием (currying) было бы

var add = function(x){return function(y){return x + y}}
add(1)(2)

Это возможно, но в процедурном языке очень мало применимо.
Так смешно читать, что для многих ФП это когда можно в одну строчку написать код…
" параллельность (выполнение одновременно, независимо) и конкурентность "
Хм, что не так с устоявшимися терминами «вытесняющая многозадачность» и «кооперативная многозадачность»?
Не так с ними то, что оба относятся к выполнению задач поочерёдно в одном потоке, псевдопараллельно — по вашей же ссылке в Вики это и написано. Разница только в том, что при кооперативной многозадачности задача сама должна отдавать управление вовне, а при вытесняющей задачи переключает ОС.
Можно как-то аргументировать минус? Или есть какие-то возражения по поводу того, что термины «вытесняющая многозадачность» и «кооперативная многозадачность» относятся к мультиплексированию потока выполнения с разделением по времени (TDM)?
Т.е., в каждый момент времени выполняется только одна задача, и разница между вытеснением и кооперативностью именно в алгоритме определения того, команды какой именно задачи в какой момент будут выполняться.
Минус ставил не я. Я пытаюсь уяснить, что вы имете ввиду, и не изобретаете ли вы заново термины.
Ваша статья написана слишком сумбурно. Человеку, не знакомому с функциональным программированием (ваша целевая аудитория, я так понимаю), она ничего не объясняет. Я сужу, конечно, по себе.
Как эти термины («параллельность» и «конкурентность») звучат на английском? Можете вы привести примеры того, что вы понимаете под параллельностью и конкурентностью?
И чтобы уж два раза не вставать, если не затруднит, приведите примеры к вот этому тоже:
1) ООП в императивном стиле — нарезать мир на плоскости последовательности состояний, и подавать их последовательно на вход простого преобразователя (вашей программы) состояний; 2) ООП в функциональном стиле — нарезается мир на последовательность преобразований, которые и применяются последовательно друг к другу, а потом этот клубок исполняется на входном состоянии (если бы в haskell был бы параллельный исполнитель, то можно было бы натравливать на входное состояние много ядер [что-то вроде fork-map-reduce], и си код стал бы тормазнутым из-за немасштабируемости).

Я не понял, как можно мир нарезать на последовательность состояний.
К сожалению, я не автор поста и не Duduka, которого вы цитируете, я просто мимо проходил.
Правда, на один из вопросов можно, вроде как, найти ответ в самом посте:
Как эти термины («параллельность» и «конкурентность») звучат на английском?
модель не готова к многопоточности (concurrency) и параллельности (parallelism)

А на другой (про последовательность состояний) могу попробовать ответить сам: речь о представлении процесса в виде последовательного изменения состояния модели, т.е. последовательности, состоящей из некоторого конечного количества её состояний. См. конечный автомат.
Прошу прощения за вопросы не по адресу, и спасибо за ответ.
По поводу последовательности: в автомате состояния представляются не в виде последовательности, а в виде сложного графа, или, иначе говоря, в виде неупорядоченного множества состояний, а также множества входов и выходов, и функции переходов и выходов. Можно записать последовательность состояний, которые принимает автомат в процессе выполнения, но она не обязательно конечная, и я плохо представляю, что значит «нарезать мир» на «плоскости последовательности состояний»… что-то у меня с воображением слабо. Ну да бог с ним.
Sign up to leave a comment.

Articles