Подтвердить что ты не робот

Что означает композиция в контексте функционального программирования?

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

Некоторые из утверждений, которые я прочитал, следующие:

  • Структуры управления не являются составными.
  • Темы не компонуются.
  • Монадические операции являются составными.
4b9b3361

Ответ 1

Марсело Кантос дал довольно хорошее объяснение, но я думаю, что его можно сделать немного точнее.

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

Комбинация структуры управления. Языки, подобные C, делают различие между выражениями, которые могут быть составлены с использованием операторов для создания новых выражений и операторов, которые могут быть составлены с использованием структур управления, таких как if, for и "структура управления последовательностями", которая просто выполняет инструкции по порядку. Дело в том, что эти две категории не находятся на равных основаниях - многие структуры управления используют выражения (например, выражение, оцененное if, чтобы выбрать, какую ветвь выполнить), но выражения не могут использовать структуры управления (например, вы не можете вернуть цикл for). Хотя может показаться сумасшедшим или бессмысленным "вернуть цикл for", на самом деле общая идея обработки управляющих структур как объектов первого класса, которые могут храниться и передаваться, не только возможна, но и полезна. В ленивых функциональных языках, таких как Haskell, структуры управления, такие как if и for, могут быть представлены как обычные функции, которые можно манипулировать в выражениях, как и любой другой термин, что позволяет использовать такие функции, которые "строят" другие функции в соответствии с параметры, которые они передают, и вернуть их вызывающему. Поэтому, в то время как C (например) делит "вещи, которые программист может захотеть делать" на две категории, и ограничивает способы объединения объектов из этих категорий, Haskell (например) имеет только одну категорию и не налагает таких ограничений, поэтому в этом смысле он обеспечивает большую гибкость.

Возможность компоновки резьбы.. Я предполагаю, что Марсело Кантос сделал это, что вы действительно говорите о композитоспособности потоков, которые используют блокировки/мьютексы. Это немного сложнее, потому что на его лице могут быть потоки, которые используют несколько блокировок; но важно то, что мы не можем иметь потоки, которые используют несколько блокировок с гарантиями, которые они должны иметь.

Мы можем определить блокировку как тип вещи, который имеет определенные операции, которые могут быть выполнены на нем, которые приходят с определенными гарантиями. Одна из гарантий: предположим, что есть объект блокировки x, тогда при условии, что каждый процесс, который вызывает lock(x), в конечном итоге вызывает unlock(x), любой вызов lock(x) в конечном итоге успешно вернется с x, заблокированным текущим потоком/обработать. Эта гарантия значительно упрощает рассуждения о поведении программы.

К сожалению, если в мире существует более одного замка, это уже не так. Если поток A вызывает lock(x); lock(y); и поток B вызывает lock(y); lock(x);, тогда возможно, что A захватывает блокировку x, а B захватывает блокировку y, и они оба будут бесконечно ждать, пока другой поток не освободит другую блокировку: deadlock. Таким образом, блокировки не являются составными, потому что, когда вы используете более одного, вы не можете просто утверждать, что эта важная гарантия по-прежнему сохраняется - , не анализируя код подробно, чтобы увидеть, как он управляет блокировками. Другими словами, вы больше не можете позволить себе рассматривать функции как "черные ящики".

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

Ответ 2

Простым примером компоновки является командная строка Linux, где символ канала позволяет объединять простые команды (ls, grep, cat и т.д.) практически неограниченным количеством способов, тем самым "составляя" большое количество сложных поведение от небольшого числа простых примитивов.

Есть несколько преимуществ для удобства:

  • Более однородное поведение: в качестве примера, имея одну команду, которая реализует "показать результаты на одной странице в время "(more), вы получаете степень равномерность пейджинга, которая не была бы возможно, если каждая команда должна была реализовать свои собственные механизмы (и флаги командной строки) для выполнения поискового вызова.
  • Меньше повторной работы по внедрению (СУХОЙ): вместо того, чтобы иметь бессмысленное различные реализации пейджинга, есть только один, который используется во всем мире.
  • Дополнительная функциональность для заданной суммы усилий по осуществлению: существующие примитивы могут быть объединены для гораздо более широкий круг задач, чем будет иметь место, если те же усилия вступил в монолитные, неконсолидируемые команды.

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

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

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

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

каждый новый тип объекта по своей сути несовместимый со всем кодом написано

что, безусловно, хорошо сделано.

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

Postscript

Эрик Раймонд написал книгу о философии Unix, два из принципов дизайна, которые он перечислял, были

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

От http://catb.org/~esr/writings/taoup/html/ch01s06.html#id2877537

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

Ответ 3

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

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

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

Ответ 4

Я согласен с ответом Марсело Кантоса, но я думаю, что он может взять на себя больше фона, чем некоторые читатели, что также связано с тем, почему композиция в функциональном программировании является особенной. Функциональный состав функций программирования по существу идентичен составу функций в математике. В математике у вас может быть функция f(x) = x^2 и функция g(x) = x + 1. Составление функций означает создание новой функции, в которой аргументы функции задаются "внутренней" функции, а выход "внутренней" функции служит в качестве входных данных для "внешней" функции. Составление f внешнего с g inner может быть записано f(g(x)). Если вы указали значение 1 для x, тогда g(1) == 1 + 1 == 2, поэтому f(g(1)) == f(2) == 2^2 == 4. В более общем плане, f(g(x)) == f(x + 1) == (x+1)^2. Я описал композицию с использованием синтаксиса f(g(x)), но математики часто предпочитают другой синтаксис, (f . g)(x). Я думаю, это происходит потому, что ясно, что f composed with g - это сама по себе функция, которая принимает один аргумент.

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

Ответ 5

Другой пример: рассмотрим асинхронное программирование в .NET. Если вы используете такой язык, как С#, и вам необходимо сделать серию асинхронных (неблокирующих) вызовов ввода-вывода через API-интерфейс Begin/End, то для последовательного вызова двух операций Foo и Bar, вы должны выставлять два метода (BeginFooAndBar, EndFooAndBar), где BeginFooAndBar вызывает BeginFoo и передает обратный вызов на Intermediate, а Intermediate затем вызывает BeginBar, и вы должны пропустить промежуточные значения и IAsyncResult передать информацию состояния, и если вы хотите, чтобы блок вокруг try - catch блокировал все это, то удачи, вам нужно дублировать код обработки исключений в трех местах, yikes, и это ужасный беспорядок.

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

let AsyncFooAndBar() = async {
    let! x = Async.FromBeginEnd(BeginFoo, EndFoo)
    let! y = Async.FromBeginEnd(BeginBar, EndBar, x)
    return y * 2 }

или что у вас есть, и это просто, и если вы хотите разместить вокруг него try - catch, отлично, код все в одном методе, а не распространяется по трем, вы просто ставите a try - catch вокруг него, и он работает.

Ответ 6

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

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

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

Ответ 7

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