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

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

Некоторые операторы этого вида, который я считал:

  • Управляющие структуры не компонуемы.
  • Потоки не сочиняют.
  • Одноместные операции компонуемы.
61
задан Surya 22 May 2010 в 04:59
поделиться

6 ответов

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

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

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

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

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

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

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

56
ответ дан 24 November 2019 в 17:14
поделиться

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

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

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

23
ответ дан 24 November 2019 в 17:14
поделиться

Другой пример: рассмотрим асинхронное программирование в .NET. Если вы используете такой язык, как C #, и вам нужно выполнить серию асинхронных (неблокирующих) вызовов ввода-вывода через 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 , отлично, код находится в одном методе, а не распространяется напротив трех, вы просто помещаете вокруг него try - catch , и он работает.

2
ответ дан 24 November 2019 в 17:14
поделиться

Я согласен с ответом Марсело Кантоса, но я думаю, что он может предполагать больше знаний, чем есть у некоторых читателей, что также связано с тем, почему композиция в функциональном программировании особенная. Состав функций функционального программирования по существу идентичен составу функций в математике. В математике у вас может быть функция f (x) = x ^ 2 и функция g (x) = x + 1 . Составление функций означает создание новой функции, в которой аргументы функции передаются «внутренней» функции, а вывод «внутренней» функции служит входом для «внешней» функции. Составив f внешний с g внутренним, можно записать 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, составленный с помощью g , является самостоятельной функцией, которая принимает единственный аргумент.

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

5
ответ дан 24 November 2019 в 17:14
поделиться

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

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

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

1
ответ дан 24 November 2019 в 17:14
поделиться

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

Композиционность имеет несколько преимуществ:

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

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

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

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

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

каждый новый тип объекта по своей природе несовместим со всем когда-либо написанным

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

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

Postscript

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

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

Из http://catb.org/~esr/writings/taoup/html/ch01s06.html#id2877537

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

32
ответ дан 24 November 2019 в 17:14
поделиться
Другие вопросы по тегам:

Похожие вопросы: