Что скажет лямбда-исчисление о возвращаемых значениях?

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

# Pseudo-code for currying
f(x,y) -> f_curried(x)(y)

Это оказалось чрезвычайно эффективным не только в изучении поведения функций, но и в практическом использовании (Haskell и т.д.).

Функции, возвращающие значения, однако, не обсуждаются. Программисты обычно имеют дело с их невозможностью вернуть более одного значения из функции, возвращая некоторый метаобъект (списки в R, структуры на С++ и т.д.). Это всегда поражало меня как немного клочья, но полезного.

Например:

# R code for "faking" multiple return values
uselessFunc <- function(dat) {
   model1 <- lm( y ~ x , data=dat )
   return( list( coef=coef(model1), form=formula(model1) ) )
}

Вопросы

  • Есть ли в лямбда-исчислении что-нибудь сказать о множестве возвращаемых значений? Если да, сделайте какие-нибудь неожиданные выводы?
  • Аналогичным образом, позволяют ли какие-либо языки использовать истинные множественные возвращаемые значения?

Ответ 1

Согласно Wikipedia страница по исчислению лямбда:

Ламбда-исчисление, также записанное как λ-исчисление, является формальной системой для функции определение, применение функций и рекурсия

И функция в математическом смысле:

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

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

Для вашего второго вопроса, насколько мне известно, языки программирования, реализующие множественные возвращаемые значения, делают это путем упаковки нескольких результатов в какую-то структуру данных (будь то кортеж, массив или даже стек), а затем распаковка это позже - и что там, где различия существуют, поскольку некоторые языки программирования делают упаковку/распаковку частью прозрачной для программиста (например, Python использует кортежи под капотом), в то время как другие языки делают программиста явно выполнять работу, например, Java-программисты могут имитируют несколько возвращаемых значений до некоторой степени, упаковывая несколько результатов в возвращаемый массив объектов, а затем извлекая и выдавая возвращенный результат вручную.

Ответ 2

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

Ответ 3

Ничто не мешает вам иметь несколько функций, каждый из которых возвращает один из нескольких результатов, которые вы хотели бы вернуть.

Например, скажем, у вас была следующая функция в python, возвращающая список.

def f(x):
  L = []
  for i in range(x):
    L.append(x * i)
  return L

Он возвращает [0, 3, 6] для x=3 и [0, 5, 10, 15, 20] для x=5. Вместо этого вы можете полностью

def f_nth_value(x, n):
  L = []
  for i in range(x):
    L.append(x * i)
  if n < len(L):
    return L[n]
  return None

Затем вы можете запросить любой из выходов для заданного ввода и получить его или получить None, если не хватает выходов:

In [11]: f_nth_value(3, 0)
Out[11]: 0

In [12]: f_nth_value(3, 1)
Out[12]: 3

In [13]: f_nth_value(3, 2)
Out[13]: 6

In [14]: f_nth_value(3, 3)

In [15]: f_nth_value(5, 2)
Out[15]: 10

In [16]: f_nth_value(5, 5)

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

def f_return_function(x):
  L = []
  for i in range(x):
    L.append(x * i)
  holder = lambda n: L[n] if n < len(L) else None
  return holder

Итак, теперь мы имеем

In [26]: result = f_return_function(5)

In [27]: result(3)
Out[27]: 15

In [28]: result(4)
Out[28]: 20

In [29]: result(5)

Традиционное нетипизированное лямбда-исчисление прекрасно способно выразить эту идею. (В конце концов, это Тьюринг завершен.) Всякий раз, когда вы хотите вернуть кучу значений, просто верните функцию, которая может дать значение n-th для любого n.

В отношении второго вопроса, python допускает такой синтаксис, если вы точно знаете, сколько значений будет возвращено функцией.

def f(x):
  L = []
  for i in range(x):
    L.append(x * i)
  return L


In [39]: a, b, c = f(3)

In [40]: a
Out[40]: 0

In [41]: b
Out[41]: 3

In [42]: c
Out[42]: 6

In [43]: a, b, c = f(2)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-43-5480fa44be36> in <module>()
----> 1 a, b, c = f(2)

ValueError: need more than 2 values to unpack

In [44]: a, b, c = f(4)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-44-d2c7a6593838> in <module>()
----> 1 a, b, c = f(4)

ValueError: too many values to unpack

Наконец, вот пример из Lisp учебник:

;; in this function, the return result of (+ x x) is not assigned so it is essentially
;; lost; the function body moves on to the next form, (* x x), which is the last form
;; of this function body. So the function call only returns (* 10 10) => 100
* ((lambda (x) (+ x x) (* x x)) 10)
=> 100
;; in this function, we capture the return values of both (+ x x) and (* x x), as the
;; lexical variables SUM and PRODUCT; using VALUES, we can return multiple values from
;; a form instead of just one
* ((lambda (x) (let ((sum (+ x x)) (product (* x x))) (values sum product))) 10)
=> 20 100

Ответ 4

Я пишу это как поздний ответ на принятый ответ, потому что это неправильно!

Lambda Calculus имеет несколько возвращаемых значений, но требуется немного понять, что означает возвращение нескольких значений.

Lambda Calculus не имеет неотъемлемого определения набора материалов, но позволяет вам изобретать его с помощью продуктов и церковных цифр.

В этом примере будет использован чистый функциональный JavaScript.

определим произведение следующим образом:

const product = a => b => callback => callback(a)(b);

тогда мы можем определить church_0, а church_1 - true, false, aka left, right, aka car, cdr, aka first, rest следующим образом:

const church_0 = a => b => a;
const church_1 = a => b => b;

начнем с создания функции, которая возвращает два значения: 20 и "Hello".

const product = a => b => callback => callback(a)(b);
const church_0 = a => b => a;
const church_1 = a => b => b;
const returns_many = () => product(20)("Hello");
const at_index_zero = returns_many()(church_0);
const at_index_one = returns_many()(church_1);

console.log(at_index_zero);
console.log(at_index_one);

Как и ожидалось, мы получили 20 и "Привет".

Чтобы вернуть более двух значений, это немного сложно:

const product = a => b => callback => callback(a)(b);
const church_0 = a => b => a;
const church_1 = a => b => b;
const returns_many = () => product(20)(
    product("Hello")(
        product("Yes")("No")
    )
);
const at_index_zero = returns_many()(church_0);
const at_index_one = returns_many()(church_1)(church_0);
const at_index_two = returns_many()(church_1)(church_1)(church_0);

console.log(at_index_zero);
console.log(at_index_one);
console.log(at_index_two);

Как вы можете видеть, функция может возвращать произвольное количество возвращаемых значений, но для доступа к этим значениям вы не можете просто использовать result() [0], result() [1] или result() [2 ], но вы должны использовать функции, которые отфильтровывают нужную позицию.

Это разумно похоже на электрические схемы, поскольку в схемах нет "0", "1", "2", "3", но у них есть средства для принятия решений и путем абстрагирования нашей схемы с байтом ( обратный список из 8 входов), слово (обратный список из 16 входов), на этом языке 0 в качестве байта будет [0, 0, 0, 0, 0, 0, 0, 0], что эквивалентно:

const Byte = a => b => c => d => e => f => g => h => callback =>
    callback(a)(b)(c)(d)(e)(f)(g)(h);

const Byte_one = Byte(0)(0)(0)(0)(0)(0)(0)(1); // preserves 
const Bit_zero = Byte_one(b7 => b6 => b5 => b4 => b3 => b2 => b1 => b0 => b0);

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

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

// represent nested list of bits(addresses) 
// to nested list of bits(bytes) interpreted as strings.
const MyArray = function(index) {
    return (index == 0)
        ? "0th"
        : (index == 1)
            ? "first"
            : "second"
        ;
};

за исключением того, что он не делает 2 ^ 32 - 1, если инструкции, он только 8 и рекурсивно сужает необходимый элемент. По сути, он действует точно так же, как мультиплексор (кроме "одиночного" сигнала на самом деле фиксированное количество бит (копроизведение, выбор), необходимое для однозначного адреса элементов).

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