Какие алгоритмы могут быть выражены с использованием общего функционального языка с параллельными операторами данных?

11

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

  • явные циклы (во всяком случае, без побочных эффектов)
  • рекурсия
  • произвольные функции первого класса (без y-комбинатора)

Язык, однако, имеет:

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

Чтобы быть более конкретным о параллельных операторах данных:

  • y = map (f, x) => y [i] = f [i]
  • y = уменьшить (f, a, x) => y = f (a, f (y [p [0]], f (y [p [1]], ...))) для некоторой перестановки p
  • y = scan (f, a, x) => y [i] = уменьшить (f, a, y [0 ... i-1])
  • y = allpairs (f, x, y) => y [i, j] = f (x [i], y [j])

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

Очевидно, есть некоторые конструкции, которые не могут быть выражены на этом языке, такие как:

while f(x) > tol:
    x <- update(x)   

Что мы можем выразить в этой системе? Мы ограничены только проблемами поиска в FP? Можем ли мы охватить все алгоритмы полиномиального времени? Кроме того, есть ли минимальный набор операторов для этого класса?

Алекс Рубинштейн
источник

Ответы:

7

Возможно, вы захотите взглянуть на старый язык программирования NESL, который серьезно относился к этим идеям. Язык основан на операциях с коллекциями, по существу списками и списками списков и т. Д., Но я думаю, что деревья и графы также рассматривались с помощью хитрых кодировок. Некоторые исследования, проведенные в этом проекте (в середине 1990-х годов), могли бы помочь ответить на ваш вопрос. Кандидатская диссертация (доступно в виде книги) Гай Э. Блллох. Векторные модели для параллельных вычислений. MIT Press, 1990 может дать некоторые ответы. Это было некоторое время назад, так как я посмотрел на это.

Работа, проделанная по формализму Bird-Meertens (BMF), также попадает в эту категорию, как и язык Charity . На странице Charity wikipedia говорится, что язык не является полным по Тьюрингу, но может выражать функцию Аккерманна, что означает, что он более чем примитивно рекурсивен. И BMF, и Charity включают такие операторы, как сгибы и сканы (катаморфизмы, анаморфизмы и т. Д.), И они имеют свои корни в теории категорий.

Короче говоря, неточный ответ заключается в том, что вы можете выразить довольно много.

Дэйв Кларк
источник
1
NESL не тотальный язык.
Per Vognsen
Некоторое время я был поверхностно осведомлен о NESL, но впервые подробно прочитал одну из статей Блелоча. Спасибо за чаевые. NESL очень похож на язык, который я описал выше, за исключением того, что, как заметил Пер Вогнсен, он допускает рекурсию.
Алекс Рубинштейн
Я также заинтересован в выборе примитивных операторов в Blelloch: map, dist (я думаю, то же самое, что я назвал 'fill'), длина, чтение массива, запись массива, сканирование, разбиение, выравнивание. Являются ли примитивы NESL "полными", или есть какая-то другая операция с параллельной реализацией данных, которая не может быть эффективно выражена с их помощью?
Алекс Рубинштейн
2
Удалите рекурсию, тогда у вас будет довольно выразительный язык, особенно если вы рассматриваете сгибы и так далее. Глядя на BMF и последующую работу, возможно, будет интереснее. Извините, но я не в курсе в этой области.
Дэйв Кларк
7

Мой другой ответ указал на недостатки языка в его нынешнем виде. В этом ответе я более подробно расскажу о проблемах сосуществования складок и разворотов на общем языке. Затем я предложу решение и покажу, что все проблемы в P (и многие другие) могут быть решены на этом расширенном языке.

Складывание на общем языке потребляет списки:

fold :: (a -> b -> b) -> b -> List a -> b

Развертывание на общем языке генерирует потоки, которые потенциально не ограничены:

unfold :: (b -> Maybe (a, b)) -> b -> Stream a

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

stream :: List a -> Stream a
list :: Int -> Stream a -> List a

Оператор потока встраивает список в ограниченный поток. Функция list извлекает первые n элементов (или меньше, если поток заканчивается раньше) в список. Таким образом, мы имеем следующее уравнение:

for all xs :: List a, xs == list (length xs) (stream xs)

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

unfoldList :: Int -> (b -> Maybe (a, b)) -> b -> List a

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

По определению, язык L находится в P, когда есть машина Тьюринга M и такой многочлен p, что членство в x в L можно определить, выполнив M не более p (n) итераций, где n = | x |. С помощью стандартного аргумента состояние ленты машины в итерации k может быть закодировано с помощью списка длиной не более 2k + 1, даже если лента машины бесконечна.

Идея теперь состоит в том, чтобы представить переход M как функцию от списков к спискам. Выполнение машины будет выполнено путем развертывания исходного состояния с помощью функции перехода. Это создает поток списков. Предположение, что L находится в P, означает, что нам нужно смотреть не дальше, чем p (n) элементов в поток. Таким образом, мы можем составить развертывание, list p(n)чтобы получить конечный список. Наконец, мы сворачиваем его, чтобы проверить, был ли ответ на решение проблемы «да» или «нет».

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

Пер Вогсен
источник
4

Это очень приукрашенный язык. Попробуйте запрограммировать факториальную функцию:

fact 0 = 1
fact n = n * fact (n-1)

Проблема в том, что ваш язык имеет только сгибы, но не разворачивается. Естественный способ выражения факториала состоит в том, чтобы складывать n в список [1, 2, ..., n] со складыванием, которое разрывает его при умножении.

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

Редактирование: Как указывает Дейв, NESL является одним из основополагающих функциональных языков параллельного программирования данных, но он очень далек от общего (даже не пытается). Семейство APL имело параллельный путь эволюции и имеет полное алгебраическое подмножество (избегайте операторов с фиксированной точкой). Если ваш вопрос сфокусирован на целостности, Дэвид Тернер написал несколько хороших статей в этой области, хотя не специально для параллельного программирования данных.

Пер Вогсен
источник
Извините, отсутствие развертываемых операторов - это упущение с моей стороны ... Я хотел добавить одного, но забыл поместить его в пост. Меня не интересует этот конкретный язык, а скорее выразительность и пределы параллельных вычислений данных.
Алекс Рубинштейн
Развертывание естественно потоковое, а не массивное, что является основной проблемой corecursion на всех строгих языках.
Per Vognsen