Pull to refresh

Comments 67

преобразование (в оригинале map)

«Map» в математическом смысле традиционно переводится на русский как отображение. Многими принято считать, что отображение — наиболее общий вариант, не накладывающий ограничений на входное и выходное множества, а область значений функции является одномерной (или R).
Спасибо, исправили!
Если есть множество чистых функций, их можно вычислять в любом порядке. Опять же, это не может повлиять на финальный результат.


Думаю, вот этот момент нуждается в пояснении, потому что банальный пример ниже показывает нам обратное (python):

def add2(value):
    return value + 2
    

def mul2(value):
    return value * 2
    
print add2(mul2(2))
print mul2(add2(2))


Функции чистые, но порядок их применения играет роль, потому что на вход в разных случаях будут подаваться разные значения.
Я думаю тут речь про то, что некоторое заданное фиксированное выражение на функциональном языке можно вычислять по-разному, и при этом будет получаться одинаковый результат. На математическом языке это называется следствием из теоремы Чёрча-Россера.
Речь шла о том, что вот такое будет иметь одинаковый результат:
# Вариант 1
res_add = add2(2)
res_mul = mul2(2)

# Вариант 2
res_mul = mul2(2)
res_add = add2(2)

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

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

У них есть как плюсы, так и минусы. Когда минусы перевешивают плюсы в F# достаточно объявить идентификатор как "mutable"


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

Сильно Linq увеличивает когнитивную нагрузку на программиста? Вроде наоборот снижает.


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

да всё он может, хватит придумывать.
Брекпоинт внутрь однострочной лямбды:
image

У них есть как плюсы, так и минусы.

По эффективности у них нет плюсов.

Когда минусы перевешивают плюсы в F# достаточно объявить идентификатор как «mutable»

Если связный список объявить как «mutable», то армотизированная сложность вычисления списка с заменой элемента внезапно станет О(1) как у нормального массива? Не в этой жизни.

Сильно Linq увеличивает когнитивную нагрузку на программиста? Вроде наоборот снижает.

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

да всё он может, хватит придумывать.

Вы как то нервно реагируете почему-то. У меня не ставится. Возможно, потому что я «забыл» для этого установить какую нибудь vs2018 на 30+ гБ. Увы, внутрь Seq.map я всё ещё попасть не могу чтобы посмотреть значение её второго аргумента. А между тем программа на F# в точности такой же набор инструкций как и её аналог на C#. по логике программистов F# отказ от возможностей отладчика по контролю выполнения кода должен дать какие-то преимущества. Хотелось бы понять — какие именно.
UFO just landed and posted this here
Для сложных, которыми любят злоупотреблять новички, читаемость ухудшается значительно, да.

По сравнению с чем? С пятью циклами и промежуточными переменными вида Dictionary<K, List<V>>?

Обычно достаточно одного цикла.
Dictionary<K, List> не потребуется писать явно, в C# есть вывод типов,
Про промежуточные переменные открою секрет — они создаются в любом случае. Даже если их нет в коде, они будут сгенерированы компилятором. Поэтому не вижу в них абсолютно ничего плохого. Промежуточные переменные упрощают отладку кода, когда один из методов делает что-то не то и хочется посмотреть на возвращаемое им значение. Они так же упрощают чтение кода и дают представление об уровне разработчика. Если промежуточные переменные имеют скверные имена, неоправданно длинные или бессмысленно короткие, значит автор кода плохо понимает решаемую им задачу или не ориентируется в предметной области. Это хороший индикатор.
И откуда же этот тип может быть, по-вашему, выведен?
Возможно, потому что я «забыл» для этого установить какую нибудь vs2018 на 30+ гБ. Увы, внутрь Seq.map я всё ещё попасть не могу чтобы посмотреть значение её второго аргумента.

Это только Ваша проблема. Можно поставить VS Code (40 мегабайт осилите?).
Тот же пример с работающим брекпоинтом в VS Code:
image


Окошко Locals:
image

Это вообще не проблема, поскольку я предпочитаю не связываться с сомнительными инструментами вроде F#. Но вы лукавите, VS Code весит что-то около 250, плюс что-то около 4-х гБ build tools. И F# плагин для неё у меня тоже не функционирует. Естественно это я неумелый, а все перечисленные инструменты идеальные, а настроить их элементарно. Видимо слишком идеальные для меня. Поэтому я предпочитаю C#, который не ломается никогда и умеет в step inside в функции на подобие Seq.map, а не только в брекпойнт на x+1
build tools на 4 Гб? Это где вы такие видели?
Посмотрел совокупный объём дискового пространства который потребовался для компиляции F# hello world на windows
UFO just landed and posted this here
Если связный список объявить как «mutable», то армотизированная сложность вычисления списка с заменой элемента внезапно станет О(1) как у нормального массива?

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


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

Не совсем так. Преимущество заключается в том, что для понимания кода (в том числе и поиска ошибок) отладчик не потребуется.


На практике отладчиком иногда приходится пользоваться и поддержка F# даже в полновесной VS действительно или оставляет желать лучшего или в силу особенностей функциональных конструкций реализация не представляется возможной (оправданной). В таких случаях я переписываю "сложный" участок в более явном виде и тогда никаких проблем нет.


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

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


Сложность конструкций также может быть связана с попыткой записать все через Linq даже если в стандартном наборе нет подходящих методов (в F# их намного больше). Вместо того чтобы использовать MoreLinq или написать собственные методы расширения новички стараются похвастаться своими умениям популярной "функциональщины". Давайте им это простим ;)

Я много раз пытался объяснить людям положительные стороны функций, но все бестолку. Они ждут что им на пальцах, за пару часов расскажут о целой парадигме. Хотя никто не ожидает что ООП можно объяснить в тот же срок. Люди годами его изучают, придумывают принципы, мнемоники и паттерны. Причем основная масса так никогда его и не осиливает. Но вот функции обязательно надо раскрыть в одной статье. Почти каждый кто стал на них писать не может нарадоваться и всячески толкает и в мир, но для вас это не показатель. Притом что им уже 60 лет. А вот очередное хайповое говно от очередного Гугла учите только так.

пс. не хочу обидеть, но это реально бесит.
UFO just landed and posted this here
Хорошо бы иметь смешанный подход в одном языке. Бывают очень нужны чистые функции для порядка.

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

Ни кто не запрещает писать вам чистые функции на любом самом что ни на есть императивном языке
UFO just landed and posted this here
UFO just landed and posted this here
вам нужно — вам и флаг в руки
UFO just landed and posted this here
UFO just landed and posted this here
в чём вы там увидели разницу?

в коменте, который вы процитировали, что то есть про гарантии?
UFO just landed and posted this here

Если в процитированном клиенте было про их отсутствие, то какой смысл в вашем вопросе "кто это гарантирует и как"? Сотрясение эфира?


" Программист должен гарантировать что не вызовет Init два раза"


Это из той же серии — что-то сказать чтобы просто сказать?

UFO just landed and posted this here
«сравнивать эти реализации по простоте понимания некорректно» — это ещё почему?

«Видимо, первый вопрос вы пропустили » — если вы про то что там якобы «сильно разные реализации, от функциональности до типобезопасности», так отвечать на этот вопрос с моей стороны означало бы признать правоту абсурдных утверждений, которые содержатся в вопросе. Обе реализации про один и тот же примитивный апи heap-а, поэтому вопрос ваш не более чем троллинг
UFO just landed and posted this here
О да, в 2018 году серьёзно — это напихать в код как можно больше stream, forest, fold, rank, flatten, map, singleton, monoid и прочих бесполезных абстракций, перегруженных методов и starship-операторов, как в этом вот коде на scala. А interface{} — это несерьёзно. Ведь код с ним слишком легко читается, а значит (с точки зрения мальчиков) это не настоящее программирование.

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


Похоже, воображение сыграло с вами злую шутку, а буйство значков навеяло морок, придав сакральную важность незначительным деталям реализации. Есть общепринятое определение кучи, с которым вы очевидно не знакомы. Из него логически следует апи, которое реализует код приведенных листингов. Всё остальное — суть костыли и оверинженеринг, как и почти всё в scala. Апи кучи не содержит требований к обходу, фильтрации и объединению контейнеров, на которых куча реализована. В Го программист волен сам выбрать структуру данных для кучи, исходя из оптимального сочетания О-сложностей операций работы с контейнером. Как правило это массив или хэш таблица. А не дерево, навязываемое маргинальной scala в качестве универсального контейнера для кучи. В Го heap может быть чём угодно, лишь бы оно удовлетворяло требованиям интерфейса heap-a — массивом, хэш-таблицей, деревом или чем то ещё. Это бонус кода на Го, заключающийся в более лучшей модульности, а не недостаток как вы тут пытаетесь это представить. Для одних контейнеров объединение c O(1) имеет место быть, для других нет. Соответственно нет нужды добавлять эту функциональность в модуль heap-a, создавая бессмысленную мешанину из логических концепций
Из него логически следует апи, которое реализует код приведенных листингов. Всё остальное — суть костыли и оверинженеринг, как и почти всё в scala...

Считаю Go слишком переусложнён, память (RAM) есть у всех. Из этого логически следует апи работы с памятью (поинтер, malloc, free). Всё остальное — суть костыли и оверинжениринг, как и почти всё в Go. Апи работы с памятью не содержит требований к работе с ней как с линкед листом, или как с деревом например, т.к. все структуры данных можно реализовать через голые поинтеры.
При прямой работе с памятью программист волен сам выбирать структуру данных для участка памяти, исходя из оптимального сочетания О-сложностей операций работы с этим участком памяти. Как правило это массив или хэш таблица. А не куча, навязываемая маргинальным Go в качестве универсального контейнера для участка памяти.
При такой работе, поинтер на участок памяти может быть чем угодно, лишь бы у вас был этот поинтер — массивом, хэш-таблицей, деревом или ещё чем-то. Это бонус кода при прямой работе с памятью, в более лучшей модульности, а не недостаток как вы тут пытаетесь это представить. Для одних участков памяти объединение c O(1) имеет место быть, для других нет. Соответственно нет нужды добавлять эту функциональность в божественные две функции — free + malloc, создавая бессмысленную мешанину из логических концепций


/s

Это что за истеричные визги?

Из этого логически следует апи работы с памятью (поинтер, malloc, free).


что вы курили?

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


это бред.

Как правило это массив или хэш таблица


именно

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


Маргинальный — судя по всему, вы, а так же F#, на котором лишь пара сайтов написана. Ценность ваших истеричных оценок Го — она соответствующая. Что Го навязывает кучу — это уже даже не бред, а шизофрения.
UFO just landed and posted this here
Какие-то нельзя?

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

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

Там ахинея, таргетированная на школьнков для гы-гы-гы, а не повторение способа аргументирования.
UFO just landed and posted this here
А вообще ничего не нужно же, и программист в том числе. Нужен только конечный результат. Так я вам его и продемонстрировал, какие вопросы (по существу)?

Я не переходил. Интерфейс, или публичный апи, доступный вызывающему коду — называйте как хотите. Погуглите что есть куча, или посмотрите комментарий авторов Го в листинге — сразу станет понятно про её интерфейс. И про то, что объединение, фильтрация и обход контейнера — это совершенно не про кучу, а привязывание этих алгоритмов непосредственно к куче — это типичный костыль
UFO just landed and posted this here
То есть, программисты не нужны, бизнес-задачи не нужны, а нужны кучи


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

То есть, Го — учебный язык, следующий шаг после языка ассемблера Дейкстры? Многое объясняет!


откуда этот бредовый вывод?

И совершенно никогда никому не нужно, да.


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

Типичное передёргивание у вас. Суть моих претензий — отсутствие в материале статьи хоть чего то практически пригодного, а не каких то там глубин Дао.

И ООП здесь совершенно не при чём. Была бы статья в стиле «ООП хорошо потому что шмяк бряк два класса привет мир», к ней бы тоже у меня возникли вопросы. Проблема не в концепциях как таковых (как правило), а в тех, кто их продвигает.

это реально бесит.


примите успокоительное.

И тем не менее:
— Иммутабельность (как и многие другие ФП-штуки) проникает в кучи языков. И прежде всего из-за того, что она позволяет сделать код надёжнее и предупредить ошибки. Их эффективность во многом обусловлена тем, насколько та или иная машина исполнения/компилятор умеет с ними работать.
— Когнитивная нагрузка опять же неоднозначна и на самом деле повторяет lurning curve типичного ФП-языка. Понять, легко читать и применять какие-нибудь «лямбды», «частичное применение», «свёртки и их производные map, filter и прочие», «композиция функций/pipe-оператор» — элементарно. И это, имхо, значительно облагораживает код по сравнению с традиционными альтернативами. Спроcите любого Android-разработчика, что он думает о retrolambda и ФП-фичах Java 8 и Kotlin'а — уверен, он вам скажет, что это глоток свежего воздуха по сравнению с тем boilerplate-ом, который навязывался до этого. Если же переходить на уровень выше, к типичным ФП-абстракциям, типа монад, функторов, стрелок, линз и дальше, то да, нужно потратить время, чтобы научиться распознавать их в типичных задачах программирования. Но это не сложнее, чем научиться распознавать паттерны GoF — просто немного иначе (и прекрасно дополняет друг друга). Более «алгебраично!» чтоли…
— Тут бы более конкретные примеры языков. Ну и не стоит забывать, что чистые функции значительно проще тестировать, а значит и количество ошибок будет меньше. Но да, определённые сложности есть.

Ну, и оглянитесь вокруг. Если даже C++ втягивает в себя концепции ФП, то это что-то но значит. Как минимум посидеть разобраться в этом стоит. Да и проекты, выполненные чисто на ФП-языках и где сработал принцип «наиболее подходящий инструмент под задачу» — есть и их всё больше.
Иммутабельность не является эксклюзивной опцией ФП и наличествует в C#. F# в плане иммутабельности привносит лишь некоторый синтаксический сахар, не более того. Это так же относится к лямбдам. Они есть практически в любом языке, непосредственного отношения к ФП не имеют, а имеют недостатки в виде доп. нагрузки на gc и рисках утечек памяти, закоторыми нужно постоянно следить. Типичному android разработчику лямбд более чем достаточно, остальные фичи котлина воспринимаются в основном как сомнительный синтаксический сахар.

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

Нет, кривая обучения здесь совершенно не при чём, поскольку в F# нет «сложных» фич ФП — ни первоклассных модулей ocaml-а, ни тайпклассов. Стандартные комбинаторы языка F# просты, их поймёт школьник даже при желании. И работающий код, в котором они интенсивно применяются, написать довольно просто. А вот читается и дебажится этот код весьма скверно из-за нелокальности символов. По моему опыту написания и ревью боевого C#/F# кода «традиционный» код с циклами for и операторами return break continue значительно проще для понимания и сопровождения.

Вообще разговор о пользе концепций разумно начинать с описания их недостатков и практических примеров, иллюстрирующих их сравнительную полезность. Доп. стат гарантии — это хорошо, но они всегда связаны с усложнением кода и росте mental cost. Это нормально, конпилятор намеренно вставляет программисту палки в колёса чтобы усложнить для него написание кода вообще и плохого кода в частности. Годные фичи усложняют написание плохого кода в большей степени, чем написание хорошего. Мой опыт в промышленной разработке на платформе .net привёл меня к тезису, противоположному тому, который задекларирован в этой статье — F# не относится к языку с таким прагматичным определением годности фич. Поскольку в нормальной дискуссии доказывает тот кто утверждает (автор), а не тот, кто опровергает (я), то хотелось бы видеть пример практического, а не академического или синтетического кода, подтверждающего полезность F#.
В исходном комментарии вы ставили вопрос о пользе ФП и его изъянах, а не конкретно F#. Я на это и ответил.
В комментариях дальше вы пытаетесь подтвердить своё мнение об «общем» своим опытом работы с «частным» — F#. «ФП — плохо, потому что я работал с F# и мне не понравилось». Как-то это не совсем корректно, не находите?
Давайте я приведу такой же частный пример с Erlang/Elixir'ом, который очень даже фп, и на котором написаны и пишутся тонны софта с высочайшими требованиями как к производительности, так и к надёжности. Но я не говорю, что этот пример доказывает величие и непогрешимость ФП.
Я специально не уточнял, да, но речь шла конечно же об ФП на F#. Я счёл это очевидным в контексте обсуждения статьи. Прошу прощения, если не вольно ввёл в заблуждение. Мои рассуждения частично можно отнести к HAskell, но про Erlang/Elixir дискутировать не готов, поскольку знаю о них на уровне проекта Эйлера. И у меня сложилось впечатление, что эти языки берут не только и не столько опциями ФП, сколько моделью многопоточности и лиспообразным REPL-ом
По моему опыту написания и ревью боевого C#/F# кода «традиционный» код с циклами for и операторами return break continue значительно проще для понимания и сопровождения.

Мы начали использовать F# год назад наравне с С# в боевом коде и внезапно обнаружили, что код на F# впоследствии приходится реже отлаживать, потому что из него не лезут бесконечные грабли с нулевыми указателями, иключениями и прочей чертовщиной. Да, всю команду пришлось заставить дружно прочитать несколько книг и провести несколько коротких хакатонов по проектированию, чтобы переключить способ мышления, потому что действительно меняется подход. Если ваш F# код написан в стиле C#, то он действительно хуже воспринимается и читается и отлаживается. Но как если он написан с использованием действительно функциональной парадигмы — то это почти сказка. В результате за год, хотя мы никого не тянули насильно, почти вся команда сама переползла на F#, а на чистом C# мы держим только WPF часть приложения.
Скажите пожалуйста, какие конкретно типичные задачи вы пытаетесь решить с помощью retrolambda или boilerplate?
UFO just landed and posted this here
Очевидно, что невозможно иметь по case-у на каждое возможное число

Для приведенного примера (т.е. для типа int) очень даже возможно.

Исходник больше 10 гиг? А есть компилятор, который подобное переварит?

Нету. Потому что на размер байт-кода тоже есть ограничения…
Как видно из диаграмм, для математической функции всегда существует только один вход и только один выход.

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

UFO just landed and posted this here
В фп нет понятия математической функции. Разработчики ФП языков ориентируются на математические ф-ции.
Если фунция принимает вектор, то да, это один объект. На самомделе можно решить что в математических ф-циях такого нет, но вот вам пример: Корень из 4 возвращает пару (-2,+2) т.е. это обычное дело возвращать множество. Смысл в том, что сопоставление всегда одно и другого быть не может.
UFO just landed and posted this here

Поиск соответствия из одного множества в другой.

Нда… Под Labview я просто устанавливаю флажок в свойствах любой функции (VI) флажок «Preallocated clone reentrant execution» и получаю такой-же эффект с распределениями по процессам, изоляцией и прочим. Только это было реализовано еще 20 лет назад.

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

Sign up to leave a comment.