Другие ответы верны - это две разные вещи. Я покажу, наоборот, они связаны. Я буду обозначать функции программирования ->
, математические функции =>
.
Предположим, у вас есть язык, который поддерживает исключения. В этом случае вы можете думать о функции программирования A -> B
как о математической функции A => B + E
, где "B + E" означает что-то типа B
или что-то типа E
. Ваш язык имеет два ключевых слова для возврата из функции, «return» и «throw».
Теперь вы можете составить две функции f: A -> B
и g: B -> C
, написав g(f(x))
. Это функция, которая принимает A и возвращает C. Вы можете сделать это на многих языках, таких как Java или Python. Под капотом, если f(x)
выдает исключение, g
не вызывается и исключение распространяется, подумайте о g(1/0)
. Язык позаботится об этом, и вам не нужно проверять, является ли результат f
исключением. Способ описать это математически, хотя f: A => B+E
и g: B => C+E
, язык "поднимает" функцию g в B+E => C+E
. g
теперь функция, которая может принимать исключение, но просто распространяет его.
Другими словами определить g': B+E => C+E
по
/ g(x) if x ∈ B
g'(x)= |
\ x if x ∈ E
Имея две функции f: A => B+E
и g': B+E => C+E
, вы можете спокойно составлять их в математическом смысле. И это то, что делает g(f(x))
в программировании, но сначала нужно поднять g
в g'
.
Подумайте о недетерминизме. Если у вас есть функция A -> B
, которая является недетерминированной, вы можете математически описать ее, сказав, что это функция A => Set(B)
, где Set(B)
- набор возможных результатов. Например, если f(1)
может дать вам 1, 2 или 3, то математически f(1) = {1,2,3}
, хотя во время программирования при запросе f(1)
вы получите только одно из этих чисел. Теперь вы можете составлять недетерминированные функции A->B
и B->C
, написав g(f(x))
. Результат будет иметь тип C
, но он будет недетерминированным. Математически, составление функций A => Set(B)
и B => Set(C)
дает вам A => Set(C)
. Хотя g
принимает только одно значение B
, вы должны поднять его до недетерминированных значений g': Set(B) => Set(C)
. Например, g'({1,2})
- это объединение множеств g(1)
и g(2)
. Таким образом, g(f(x))
означает, что вы запускаете f
, берете набор всех возможных результатов и запускаете g
для каждого из них. Есть два слоя недетерминизма, но они «сплющены» в один.
То же самое с глобальным состоянием. Если вы сделаете каждую глобальную переменную в качестве аргумента функции и результата, вы можете подумать, что глобальных переменных не существует, каждая функция принимает все глобальное состояние, и язык должен передать это состояние при компоновке. Функция A -> B
чтения и, возможно, записи состояния S
является функцией (A,S) => (B,S)
, которая также может быть записана как A => (S => (B,S))
. Написание этого формально сложнее, но это тот же шаблон.
То же самое можно сделать с помощью ввода / вывода, я описал это в другом SO ответе .
"Магия", которая позволяет создавать "эффективные" функции:
- Способ сделать тип эффективным. Например, включите
B
в B+E
или Set(B)
. Я обозначу эффективную версию типа X
как F(X)
.
- Способ поднять функции:
B -> F(C)
в F(B) -> F(C)
. Позволяет составлять функции A -> F(B)
и B -> F(C)
.
- Ключевое слово
return
должно превратить обычное значение A
в A+E
или singleton Set(A)
. Так что это тип должен быть X -> F(X)
.
Структура, состоящая из этих трех, называется монадой. Монады позволяют описать эти побочные эффекты. Монада также может иметь некоторые специфические функции, например, монада исключений имеет throw
, монада недетерминизма имеет fork
, монада состояний имеет get/put
, монада ввода-вывода имеет read/write
и т. Д.
Мораль такова: если вы рассматриваете специальные эффекты, такие как рандомизация, исключения, недетерминизм, ввод / вывод, как часть результата функции, то каждая функция прозрачна по ссылкам, а функции в императивном программировании - это действительно математические функции, но с очень странными типами результатов, которые также описывают спецэффекты.Это подход, используемый чистыми функциональными языками, такими как Haskell.