Что такое ссылочная прозрачность?

38

Я видел это в императивных парадигмах

F (X) + F (х)

может не совпадать с:

2 * Р (х)

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

Какой пример мог бы указать на разницу с данной функцией?

Asgard
источник
7
Вы можете писать часто прозрачные функции в Python и часто делаете это. Разница в том, что язык не обеспечивает его соблюдение.
Карл Билефельдт
5
в С и так: f(x++)+f(x++)может не совпадать с 2*f(x++)(в С это особенно замечательно, когда подобные вещи прячутся в макросах - разве я сломал себе нос по этому поводу? Бьюсь об заклад)
комнат
В моем понимании, пример @ gnat - это то, почему функционально-ориентированные языки, такие как R, используют передачу по ссылке и явно избегают функций, которые изменяют свои аргументы. В R, по крайней мере, на самом деле может быть трудно обойти эти ограничения (по крайней мере, стабильным, переносимым способом), не углубляясь в сложную систему языков, пространства имен и пути поиска.
борец с тенью
4
@ssdecontrol: На самом деле, когда у вас есть ссылочная прозрачность, передача по значению и передача по ссылке всегда дают один и тот же результат, поэтому не имеет значения, какой язык использует. Функциональные языки часто определяются с чем-то вроде передачи по значению для семантической ясности, но их реализации часто используют передачу по ссылке для производительности (или даже оба, в зависимости от того, какой из них быстрее для данного контекста).
Йорг Миттаг
4
@gnat: В частности, f(x++)+f(x++)может быть абсолютно любым, поскольку вызывает неопределенное поведение. Но на самом деле это не относится к ссылочной прозрачности - что не помогло бы для этого вызова, оно «неопределено» для ссылочно-прозрачных функций, как и в sin(x++)+sin(x++). Может быть 42, мог отформатировать ваш жесткий диск, мог бы иметь демонов, летящих из носа пользователей ...
Кристофер Кройциг

Ответы:

62

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

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

counter = 0

def foo(x):
  global counter

  counter += 1
  return x + counter

не является ссылочно прозрачным, фактически вызывает

foo(x) + foo(x)

а также

2 * foo(x)

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

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

Итак, для любой функции Haskell

f :: Int -> Int

и любое целое число x, это всегда верно, что

2 * (f x) == (f x) + (f x)

Примером действия является результат библиотечной функции getLine:

getLine :: IO String

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

[getLine, getLine] :: [IO String]

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

main = <some action>

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

Благодаря системе типов Haskell действие никогда не может использоваться в контексте, где ожидается другой тип, и наоборот. Итак, если вы хотите найти длину строки, вы можете использовать lengthфункцию:

length "Hello"

вернет 5. Но если вы хотите найти длину строки, считываемой из терминала, вы не можете написать

length (getLine)

потому что вы получаете ошибку типа: lengthожидается ввод списка типов (а String - это действительно список), но getLineэто значение типа IO String(действие). Таким образом, система типов гарантирует, что значение действия наподобие getLine(чье выполнение выполняется за пределами основного языка и которое может быть нереферентно прозрачным) не может быть скрыто внутри значения типа без действия Int.

РЕДАКТИРОВАТЬ

Чтобы ответить на вопрос exizt, вот небольшая программа на Haskell, которая читает строку из консоли и печатает ее длину.

main :: IO () -- The main program is an action of type IO ()
main = do
          line <- getLine
          putStrLn (show (length line))

Основное действие состоит из двух подзадач, которые выполняются последовательно:

  1. getlineтипа IO String,
  2. второе строится путем оценки функции putStrLnтипа String -> IO ()по ее аргументу.

Точнее, второе действие построено

  1. привязка lineк значению, прочитанному первым действием,
  2. вычисление чистых функций length(вычислить длину как целое число) и затем show(превратить целое число в строку),
  3. построение действия путем применения функции putStrLnк результату show.

На этом этапе второе действие может быть выполнено. Если вы ввели «Hello», он напечатает «5».

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

main = do
          line <- getLine
          show (length line) -- Error:
                             -- Expected type: IO ()
                             --   Actual type: String

потому что show (length line)имеет тип, Stringтогда как запись do требует, чтобы за действием ( getLineтипа IO String) следовало другое действие (например, putStrLn (show (length line))типа IO ()).

РЕДАКТИРОВАТЬ 2

Определение реляционной прозрачности Йоргом В. Миттагом является более общим, чем мое (я одобрил его ответ). Я использовал ограниченное определение, потому что пример в вопросе фокусируется на возвращаемом значении функций, и я хотел проиллюстрировать этот аспект. Однако RT в целом относится к смыслу всей программы, включая изменения глобального состояния и взаимодействия с окружающей средой (IO), вызванные оценкой выражения. Итак, для правильного общего определения вы должны обратиться к этому ответу.

Джорджио
источник
10
Может ли downvoter предложить, как я могу улучшить этот ответ?
Джорджио
Итак, как можно получить длину строки, прочитанной с терминала в Haskell?
сбиченко
2
Это крайне педантично, но ради полноты, это не система типов Хаскелла, которая гарантирует, что действия и чистые функции не смешиваются; Дело в том, что язык не предоставляет никаких нечистых функций, которые вы можете вызывать напрямую. На самом деле вы можете IOдовольно легко реализовать тип языка Haskell на любом языке с лямбдами и обобщениями, но поскольку каждый может позвонить printlnнапрямую, реализация IOне гарантирует чистоту; это будет просто соглашение.
Довал
Я имел в виду, что (1) все функции чисты (конечно, они чисты, потому что язык не предоставляет никаких нечистых, хотя, насколько я знаю, есть некоторые механизмы, чтобы обойти это), и (2) чистые функции и нечистые действия имеют разные типы, поэтому их нельзя смешивать. Кстати, что вы подразумеваете под звонком напрямую ?
Джорджио
6
Ваша точка зрения о том, что вы getLineне относитесь к прозрачности, неверна. Вы представляете, getLineкак будто он оценивает или уменьшает некоторую строку, конкретная строка которой зависит от ввода пользователя. Это неверно IO Stringне содержит String больше, чем Maybe Stringделает. IO Stringэто рецепт для, возможно, получения String и, как выражение, он такой же чистый, как и любой другой в Haskell.
LuxuryMode
25
def f(x): return x()

from random import random
f(random) + f(random) == 2*f(random)
# => False

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

Взять, к примеру, следующую программу:

def f(): return 2

print(f() + f())
print(2)

Эта программа является прозрачной. Я могу заменить один или оба вхождения f()с , 2и она будет работать так же:

def f(): return 2

print(2 + f())
print(2)

или

def f(): return 2

print(f() + 2)
print(2)

или

def f(): return 2

print(2 + 2)
print(f())

все будут вести себя одинаково.

Ну, вообще-то, я обманул. Я должен иметь возможность заменить вызов на printего возвращаемое значение (которое вообще не имеет значения) без изменения смысла программы. Однако, ясно, что если я просто уберу два printутверждения, смысл программы изменится: раньше она выводила что-то на экран, а после этого нет. Ввод / вывод не прозрачен.

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

Йорг Миттаг
источник
4
«не может иметь никакого изменяемого состояния»: ну, вы можете иметь его, если оно скрыто и не влияет на наблюдаемое поведение вашего кода. Подумайте, например, о запоминании.
Джорджио
4
@ Джорджио: Возможно, это субъективно, но я бы сказал, что кэшированные результаты на самом деле не являются "изменяемым состоянием", если они скрыты и не имеют видимых эффектов. Неизменность - это всегда абстракция, реализованная поверх изменяемого оборудования; часто это обеспечивается языком (давая абстракцию «значения», даже если значение может перемещаться между регистрами и ячейками памяти во время выполнения, и может исчезать, если известно, что оно никогда не будет использоваться снова), но оно не менее допустимо, когда оно предоставленный библиотекой или еще много чего. (Конечно, при условии, что оно реализовано правильно.)
ruakh
1
+1 Мне очень нравится printпример. Возможно, один из способов увидеть это состоит в том, что то, что напечатано на экране, является частью «возвращаемого значения». Если вы можете заменить printего возвращаемым значением функции и эквивалентной записью на терминале, пример работает.
Пьер Арло
1
@ Джорджио Использование пространства / времени не может рассматриваться как побочный эффект в целях прозрачности ссылок. Это сделало бы 4и 2 + 2не взаимозаменяемыми, так как они имеют разное время выполнения, и весь смысл ссылочной прозрачности в том, что вы можете заменить выражение тем, к чему оно относится. Важным соображением будет безопасность потоков.
Довал
1
@overexchange: ссылочная прозрачность означает, что вы можете заменить каждое подвыражение его значением, не меняя смысла программы. listOfSequence.append(n)возвращается None, так что вы должны быть в состоянии заменить каждый вызов listOfSequence.append(n)с Noneбез изменения смысла программы. Вы можете сделать это? Если нет, то он не является ссылочно прозрачным.
Йорг Миттаг
1

Части этого ответа взяты из незавершенного учебника по функциональному программированию , размещенного на моей учетной записи GitHub:

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

Рассмотрим простой пример:

x = 42

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

Из Haskell вики :

Чистые вычисления дают одно и то же значение каждый раз, когда они вызываются. Это свойство называется ссылочной прозрачностью и позволяет проводить эквациональные рассуждения по коду ...

Чтобы контрастировать с этим, тип операции, выполняемой C-подобными языками, иногда называют деструктивным назначением .

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

  • не разрешается проявлять какие-либо побочные эффекты, и
  • он должен быть ссылочно прозрачным.

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

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

yesthisisuser
источник
кажется, это начинается с словом за словом копию взято отсюда : «Функция называется референциально прозрачным , если она, учитывая те же входные параметры, всегда производит тот же результат ...» Стек биржа правила плагиат , является вы знаете об этом? «Плагиат - это бездушный акт: копировать куски чужой работы, ставить на нее свое имя и выдавать себя за оригинального автора ...»
комнат
3
Я написал эту страницу.
yesthisisuser
если это так, рассмотрите возможность сделать его менее плагиатом - потому что читатели не могут сказать. Вы знаете, как это сделать в SE? 1) Вы ссылаетесь на источник оригиналов, например, «Как (я) написал [here](link to source)...», а затем 2) правильное форматирование цитат (используйте кавычки, или еще лучше,> символ). Также не помешало бы, если бы помимо общих указаний, отвечал на конкретные вопросы, о которых идет речь, в данном случаеf(x)+f(x) / 2*f(x), см. Как ответить - в противном случае может показаться, что вы просто рекламируете свою страницу
gnat
1
Теоретически я понял этот ответ. Но, практически следуя этим правилам, мне нужно вернуть список последовательности града в этой программе . Как мне это сделать?
сверхобмена