Основы функционального программирования/Структуры данных и базисные операции — 2: различия между версиями
Содержимое удалено Содержимое добавлено
Стилистика |
м Форматирование формул |
||
Строка 6:
== Типы в функциональных языках ==
В первой лекции говорилось, что аргументами функций могут быть не только переменные базовых типов, но и другие функции. В этом случае появляется понятие [[w:Функция высшего порядка|функций высшего порядка]]. Но для рассмотрения функций высшего порядка необходимо ввести понятие функционального типа (или тип функции). Пусть некоторая функция <math>f</math> — это функция одной переменной из множества <math>A</math>, принимающая значение из множества <math>B</math>. Тогда по определению:
<center><math>\#(f) : A \rightarrow B</math></center>
Знак <math>\#(f)</math> обозначает «тип функции <math>f</math>». Таким образом, типы, в которых есть символ стрелки <math>\rightarrow</math>, носят название функциональных типов. Иногда для их обозначения используется более
Например: <math>\#(\sin) : \operatorname{Float} \rightarrow \operatorname{Float}</math>, <math>\#(\operatorname{length}) : \operatorname{List}(A) \rightarrow \operatorname{Integer}</math>
Для функций многих аргументов определение типа можно выводить при помощи операции декартового произведения (например, <math>\#(\operatorname{add}(x, y)) : \operatorname{Float} \times \operatorname{Float} \rightarrow \operatorname{Float}</math>). Однако в функциональном программировании такой способ определения типов функций многих переменных не прижился.
В 1924 году [[w:Шёнфинкель, Мозес|М. Шёнфинкель]] предложил представлять функции многих аргументов как последовательность функций одного аргумента. В этом случае тип функции, которая складывает два действительных числа, выглядит так: <math>\operatorname{Float} \rightarrow (\operatorname{Float} \rightarrow \operatorname{Float})</math>.
'''Пример 8. Тип функции <math>\operatorname{add}(x, y)</math>.'''
Предположительно, каждый из аргументов функции <math>\operatorname{add}</math> уже́ означен, пусть <math>x = 5</math>, <math>y = 7</math>. В этом случае из функции <math>\operatorname{add}</math> путём удаления первого аргумента получается новая функция — <math>\operatorname{add5}</math>, которая прибавляет к своему единственному аргументу число <math>5</math>. Её тип получается легко, он по определению таков: <math>\operatorname{Float} \rightarrow \operatorname{Float}</math>. Теперь, возвращаясь назад, можно понять, почему тип функции <math>\operatorname{add}</math> равен <math>\operatorname{Float} \rightarrow (\operatorname{Float} \rightarrow \operatorname{Float})</math>.
Для того чтобы не изощряться с написанием функций типа <math>\operatorname{add5}</math> (как в предыдущем примере), была придумана специальная аппликативная форма записи в виде «оператор-операнд». Предпосылкой для этого послужило новое видение на функции в функциональном программировании. Ведь если традиционно считалось, что выражение <math>f(5)</math> обозначает «применение функции <math>f</math> к значению аргумента, равному <math>5</math>» (
Таким образом, если функция <math>f</math> имеет тип <math>
Соответственно выражение, в котором все функции рассматриваются как функции одного аргумента, а единственной операцией является аппликация (применение), называются выражениями в форме «оператор-операнд». Такие функции получили название «каррированные», а сам процесс сведения типа функции к виду, приведённому в предыдущем абзаце — каррированием (по имени [[w:Карри, Хаскелл|Карри Хаскелла]]).
В λ-исчислении также присутствует математическая абстракция для аппликативных форм записей. Например:
<math>\begin{array}{rcl}
\end{array}</math>
▲<center><math>f (x, y) = x + y \Rightarrow \lambda y.\lambda x.(x + y)</math></center>
▲<center><math>f (x, y, z) = x^{2} + y^{2} + z^{2} \Rightarrow \lambda z.\lambda y.\lambda x.(x^{2} + y^{2} + z^{2})</math></center>
▲И так далее...
== Несколько слов о нотации абстрактного языка ==
Строка 42:
=== Образцы и клозы ===
Необходимо отметить, что в нотации абстрактного функционального языка, который использовался до сих пор для написания примеров функций, можно было бы использовать такую конструкцию, как <math>\operatorname{if}-\operatorname{then}-\operatorname{else}</math>. Например, при описании функции <math>\operatorname{append}</math> (см. '''[[Основы функционального программирования/Структуры данных и базисные операции#Примеры|пример 7]]'''), её тело можно было бы записать следующим образом:
<math>\operatorname{append}(
Однако данная запись чревата непониманием и трудным разбором. Поэтому даже в '''[[Основы функционального программирования/Структуры данных и базисные операции#Примеры|примере 7]]''' была использована нотация, которая поддерживает так называемые «образцы».
Строка 59:
*<math>[X, Y]</math> — список.
К образцам предъявляется единственое требование, без которого сопоставление с образцами может быть выполнено неверно. Требование это звучит следующим образом: при сопоставлении образца с данными означивание переменных должно происходить единственным образом.
Кроме образцов в функциональном программировании вводится такое понятие, как «клоз» (от англ. «clause»). По определению клозы выглядят так:
<math>\mathbf{def} f
где:
Строка 69:
*<math>\mathbf{def}</math> и <math>=</math> — константы абстрактного языка;
*<math>f</math> — имя определяемой функции;
*<math>
*<math>\operatorname{expr}</math> — выражение.
Таким образом, определение функции в функциональном программировании есть просто последовательность клозов (возможно состоящая из одного элемента). Для того, чтобы упростить запись определений функций, далее слово <math>\mathbf{def}</math> будет опускаться.
'''Пример 9. Образцы и клозы в функции <math>\operatorname{length}</math>.'''
<math>\operatorname{length}([\,]) = 0</math>
<math>\operatorname{length}(L) = 1 + \operatorname{length}(\operatorname{tail}(L))</math>
Пусть вызов функции <math>\operatorname{length}</math> произведён с параметром <math>[a, b, c]</math>. В этом случае начнёт свою работу механизм сопоставления с образцом. Последовательно перебираются все клозы и делаются попытки сопоставления. В данном случае удачное сопоставление будет только во втором клозе (
Интерпретация вызова функции заключается в нахождении первого по порядку сверху вниз образца, успешно сопоставимого с фактическим параметром. Значение переменных образца, означиваемые в результате сопоставления, подставляются в правую часть клоза (выражение <math>\operatorname{expr}</math>), вычисленное значение которой в данном контексте объявляется значением вызова функции.
=== Охрана ===
При написании функций в абстрактной нотации допустимо использовать так называемую охрану,
<math>\operatorname{length}(L) = 0\ \mathbf{when}\ L = [\,]</math>
<math>\operatorname{length}(L) = 1 + \operatorname{length}(\operatorname{tail}(L))\ \mathbf{otherwise}</math>
В рассмотренном коде слова́ <math>\mathbf{when}</math> (когда) и <math>\mathbf{otherwise}</math> (в противном случае) являются зарезервированными словами языка. Однако использование этих слов не является необходимым условием для организации охраны. Охрану можно организовывать различными способами, в том числе и с помощью λ-исчисления:
<math>\operatorname{append} = \lambda [\,].(\lambda L.L)</math>
<math>\operatorname{append} = \lambda (h:t).(\lambda L.h:\operatorname{append}(t, L))</math>
Представленная запись не очень читабельна, поэтому использоваться она будет только в крайних случаях по необходимости.
Строка 102:
=== Локальные переменные ===
Как уже́ говорилось, использование локальных переменных представляет собой побочный эффект, поэтому оно недопустимо в функциональных языках. Однако в некоторых случаях использование локальных переменных носит оптимизирующий характер, что позволяет сэкономить время и ресурсы во время вычислений.
Пусть <math>f</math> и <math>h</math> — функции, и необходимо вычислить выражение <math>h\Big(f(X),f(X)\Big)</math>. Если в языке нет оптимизирующих методов, то в этом случае произойдёт двойное вычисление выражения <math>f(X)</math>. Чтобы этого не произошло, можно прибегнуть к такому изощренному способу: <math>\Big(\lambda v.h(v, v)\Big)\Big(f(X)\Big)</math>. Естественно, что в этом случае выражение <math>f(X)</math> вычислится первым и один раз. Для того, чтобы минимизировать использование λ-исчисления, далее будет применяться следующая форма записи:
<math>\mathbf{let}\ v = f(X)\ \mathbf{in}\ h(v, v)</math>
(слова́ <math>\mathbf{let}</math>, <math>=</math> и <math>\mathbf{in}</math> — зарезервированы в языке). В этом случае переменная <math>v</math> будет называться локальной.
== Элементы программирования ==
Строка 114:
=== Накапливающий параметр — аккумулятор ===
Бывает так, что при выполнении функции исключительно
<math>\operatorname{Factorial}(0) = 1</math>
<math>\operatorname{Factorial}(N) = N
Если провести пример вычисления этой функции с аргументом <math>3</math>, то можно будет увидеть следующую последовательность:
<math>\operatorname{Factorial}(3)</math>
<tt>==></tt> <math>3
<tt>==></tt> <math>3
<tt>==></tt> <math>3
<tt>==></tt> <math>3
<tt>==></tt> <math>3
<tt>==></tt> <math>3
<tt>==></tt> <math>6</math>
Строка 144:
'''Пример 10. Функция вычисления факториала с аккумулятором.'''
<math>
<math>F(0, A) = A</math>
<math>F(N, A) = F((N - 1), (N
В этом примере второй параметр функции <math>F</math> выполняет роль аккумулирующей переменной, именно в ней содержится результат, который возвращается по окончании рекурсии. Сама же рекурсия в этом случае принимает вид «хвостовой», память при этом расходуется только на хранение адресов возврата значения функции.
Строка 165:
Возникает вопрос: любую ли функцию можно преобразовать для вычисления с аккумулятором? Очевидно, что ответ на этот вопрос отрицателен. Построение функций с накапливающим параметром — приём не универсальный, и он не гарантирует получения хвостовой рекурсии. С другой стороны, построение определений с накапливающим параметром является делом творческим. В этом процессе необходимы некоторые эвристики.
'''Определение:'''
Общий вид рекурсивных определений, позволяющих при трансляции обеспечить вычисления в постоянном объёме памяти через итерацию, называется равенствами в итеративной форме.
Строка 171:
Общий вид равенств в итеративной форме может быть описан следующим образом:
<center><math>
При этом на выражения <math>e_{ij}</math> накладываются следующие ограничения:
#<math>e_{ij}</math> — «простое» выражение,
#<math>e_{ij}</math> имеет вид <math>
#<math>e_{ij}</math> — условное выражение с простым выражением в условии, ветви которого определяются этими же тремя пунктами.
Строка 182:
#Построить функции, работающие со списками. При необходимости воспользоваться дополнительными функциями и теми, что определены в '''[[Основы функционального программирования/Структуры данных и базисные операции|лекции 2]]'''.
#*<math>\operatorname{ReverseAll}</math> — функция, получающая на вход списочную структуру и обращающая все её элементы, а также её саму.
#*<math>\operatorname{Position}</math> — функция, возвращающая номер первого вхождения заданного атома в список.
#*<math>\operatorname{Set}</math> — функция, возвращающая список из всех атомов, содержащихся в заданном списке. Каждый атом должен присутствовать в результирующем списке в единственном числе.
#*<math>\operatorname{Frequency}</math> — функция, возвращающая список пар (символ, частота). Каждая пара определяет атом из заданного списка и частоту его вхождения в этот список.
#Написа́ть (если это возможно) функции с накапливающим параметром для заданий из '''[[Основы функционального программирования/Структуры данных и базисные операции#Упражнения|упражнения 1]]''' '''[[Основы функционального программирования/Структуры данных и базисные операции|лекции 2]]'''.
Строка 191:
#Следующие описания реализуют заявленные функции. В некоторых пунктах реализованы дополнительные функции, обойтись без которых представляется сложным.
##<math>\operatorname{ReverseAll}</math>:
##*<math>\operatorname{ReverseAll}(L) = L\ \mathbf{when}\ \operatorname{atom}(L)</math>
##*<math>\operatorname{ReverseAll} ([\,]) = [\,]</math>
##*<math>\operatorname{ReverseAll}(H:T) =
##<math>\operatorname{Position}</math>:
##*<math>\operatorname{Position}(A, L) =
##*<math>
##*<math>
##*<math>
##<math>\operatorname{Set}</math>:
##*<math>\operatorname{Set}([\,]) = [\,]</math>
##*<math>\operatorname{Set}(H:T) = \operatorname{Include}(H, \operatorname{Set}(T))</math>
##*<math>\operatorname{Include}(A, [\,]) = [A]</math>
##*<math>\operatorname{Include}(A, A:L) = A:L</math>
##*<math>\operatorname{Include}(A, B:L) = \operatorname{prefix}(B, \operatorname{Include}(A, L))</math>
##<math>\operatorname{Frequency}</math>:
##*<math>\operatorname{Frequency}(L) = F([\,], L)</math>
##*<math>F(L, [\,]) = L</math>
##*<math>F(L, H:T) = F(\operatorname{Corrector}(H, L), T)</math>
##*<math>\operatorname{Corrector}(A, [\,]) = [A:1]</math>
##*<math>\operatorname{Corrector}\Big(A, (A:N):T\Big) = \operatorname{prefix}\Big((A:N + 1), T\Big)</math>
##*<math>\operatorname{Corrector}(A, H:T) = \operatorname{prefix}(H, \operatorname{Corrector}(A, T))</math>
#Для всех функций из '''[[Основы функционального программирования/Структуры данных и базисные операции#Упражнения|упражнения 1]]''' '''[[Основы функционального программирования/Структуры данных и базисные операции|лекции 2]]''' можно построить определения с накапливающим параметром. С другой стороны, возможно некоторые из вновь построенных функций не будут оптимизированы.
##<math>\operatorname{Power\_A}</math>:
##*<math>\operatorname{Power\_A}(X, N) = P(X, N, 1)</math>
##*<math>P(X, 0, A) = A</math>
##*<math>P(X, N, A) = P(X, N - 1, X
##<math>\operatorname{Summ\_T\_A}</math>:
##*<math>\operatorname{Summ\_T\_A}(N) = \operatorname{ST}(N, 0)</math>
##*<math>\operatorname{ST}(0, A) = A</math>
##*<math>\operatorname{ST}(N, A) = \operatorname{ST}(N - 1, N + A)</math>
##<math>\operatorname{Summ\_P\_A}</math>:
##*<math>\operatorname{Summ\_P\_A}(N) = \operatorname{SP}(N, 0)</math>
##*<math>\operatorname{SP}(0, A) = A</math>
##*<math>\operatorname{SP}(N, A) = \operatorname{SP}(N - 1, \operatorname{Summ\_T\_A}(N) + A)</math>
##<math>\operatorname{Summ\_Power\_A}</math>:
##*<math>\operatorname{Summ\_Power\_A}(N, P) = \operatorname{SPA}(N, P, 0)</math>
##*<math>\operatorname{SPA}(N, 0, A) = A</math>
##*<math>\operatorname{SPA}(N, P, A) = \operatorname{SPA}\left(N, P - 1,
##<math>\operatorname{Exponent\_A}</math>:
##*<math>\operatorname{Exponent\_A}(N, P) = \operatorname{E}(N, P, 0)</math>
##*<math>\operatorname{E}(N, 0, A) = A</math>
##*<math>\operatorname{E}\left(N, P - 1,
|