Безгражданство подразумевает ссылочную прозрачность?

#haskell #stateless #referential-transparency

Вопрос:

У меня есть этот код

 data Slist a = Empty | Scons (Sexp a) (Slist a) 
data Sexp a = AnAtom a | AnSlist (Slist a)
data Fruit = Peach | Apple | Pear | Lemon | Fig deriving (Show,Eq)

sxOccurs oatm sxp =
  let slOC Empty = 0
      slOC (Scons se sls) = (seOC se)   (slOC sls)
      seOC (AnAtom atm) = if (atm == oatm) then 1 else 0
      seOC (AnSlist sla) = slOC sla
  in seOC sxp
 

Как вы можете видеть, у sxOccurs меня внутри есть две вспомогательные функции let , которые являются «взаимно самореферентными», как их называет мой Маленький MLer: slOC и seOC . Поэтому в SML вы должны использовать ключевое and слово, чтобы сообщить им друг о друге и «перекрестно ссылаться» друг на друга. Кстати, sxOccurs подсчитывает количество конкретных AnAtom объектов в s-списке, в моем примере атомы являются Fruit переменными.

Мой вопрос в том, является ли это примером ссылочной прозрачности? Аналогично, в Дэви он приводит такой пример

 let s0 = emptyStack
    s1 = push 12.2 s0
    s2 = push 7.1 s1
    s3 = push 6.7 s2
    s4 = divStack s3
    s5 = push 4.3 s4
    s6 = subtStack s5
    s7 = multStack s6
    s8 = push 2.2 s7
    s9 = addStack s8
in popStack s9
 

отмечая, что стек в Imperative-land постоянно изменяет стек, в то время как Haskell создает новую si переменную для каждой операции стека. Затем он говорит, что каждую из этих строк можно было бы перетасовать в другом порядке, и результат остался бы неизменным. AFAICT это та же основная идея, что и у меня sxOccurs , когда неважно, в каком порядке я представляю подфункции. Итак, опять же, является ли это более глубоким значением ссылочной прозрачности? Если нет, то что это я здесь показываю?

Комментарии:

1. «Ссылочная прозрачность» означает «один и тот же ввод дает один и тот же результат».

2. Тогда что же я описал выше?

3. Первый пример — «взаимная рекурсия», а во втором я не уверен, что для этого есть специальный термин.

Ответ №1:

Ссылочная прозрачность означает это, и только это: вы можете заменить переменную по ее определению, не изменяя значения программы. Это называется «ссылочной прозрачностью», потому что вы можете «просмотреть» ссылку на ее определение.

Например, вы пишете:

 slOC Empty = 0
slOC (Scons se sls) = (seOC se)   (slOC sls)
seOC (AnAtom atm) = if (atm == oatm) then 1 else 0
seOC (AnSlist sla) = slOC sla
 

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

 -- replace slOC by its definition
seOC (AnSlist sla) = (v -> case v of Empty -> 0; SCons se sls -> seOC se   slOC sls) sla
-- replace slOC by its definition *again*, starting from the previous line
seOC (AnSlist sla) = (v -> case v of
    Empty -> 0
    SCons se sls -> seOC se   (v -> case v of
        Empty -> 0
        SCons se sls -> seOC se   slOC sls
        ) sls
    ) sla
-- replace slOC by its definition in another equation
slOC (Scons se sls) = seOC se   (v -> case v of Empty -> 0; SCons se sls -> seOC se   slOC sls) sls
-- or, you could replace seOC by its definition instead
slOC (SCons se sls) = (v -> case v of
    AnAtom atm -> if atm == oatm then 1 else 0
    AnSlist sla -> sLOC sla
    ) se   slOC sls
-- or you could do both, of course
 

Ну, конечно, верно? К настоящему времени вы, возможно, думаете: «Но, Дэниел, как это свойство могло когда-либо потерпеть неудачу?». Я кратко перейду к другому языку, чтобы проиллюстрировать: C.

 int *x = malloc(sizeof(*x));
x[0] = 42;
printf("%dn", x[0]);
 

В случае, если вы плохо читаете C , это создает новую переменную с именем x , выделяет для нее некоторое пространство, записывает 42 в это пространство, а затем выводит значение, хранящееся в этом пространстве. (Мы, вероятно, должны ожидать, что он будет напечатан 42 !) Но я определил x = malloc(sizeof(*x)) в первой строке; могу ли я заменить x это определение в другом месте?

Нет! Это совсем другая программа:

 int *x = malloc(sizeof(*x));
malloc(sizeof(*x))[0] = 42;
printf("%dn", x[0]);
 

Это все еще синтаксически корректная программа, но теперь x[0] она не была инициализирована в тот момент, когда мы достигаем строки, которая ее печатает, потому что мы выделили второй, независимый кусок пространства и инициализировали вместо этого другое пространство.

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

Ответ №2:

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

 global_var = 0

def my_function():
    return global_var

my_function() # 0
global_var = 100
my_function() # 100
 

Мы позвонили my_function с теми же входными данными, но он таинственным образом выдал другой результат. Теперь, конечно, в этом примере очевидно, почему это так, но идея ссылочной прозрачности заключается в том, что в реальном коде это не будет настолько очевидным. Если язык, на котором вы работаете, не имейте ссылочную прозрачность, и действительно, если язык поощряет мутацию стиля действия на расстоянии, то вы неизбежно получите функции, которые обращаются к изменяемому состоянию, о котором вы не знаете. Хорошо написанная функция будет включать обширную документацию об этих угловых случаях, но если вы когда-либо работали с какой-либо базой кода среднего или большего размера, вы знаете, что «хорошо документированная функция» — это редкое зрелище.

В Haskell нет способа* написать функцию, подобную приведенной выше функции Python. В худшем случае мы можем завернуть его в IO

 myFunction :: IORef Int -> IO Int
myFunction = readIORef
 

Но теперь одна только подпись типа с первого взгляда говорит нам: «здесь происходит что-то подозрительное; покупатель остерегается», и даже тогда мы можем получить доступ только к одной глобальной переменной IORef , к которой она дает нам доступ.

*В Haskell невозможно написать функцию , кроме как использовать unsafePerformIO , за которой стоит множество драконов. С unsafePerformIO помощью этого мы можем довольно очевидно нарушить ссылочную прозрачность , поэтому в модуле под названием «небезопасно» есть функция под названием «небезопасно», о которой каждый учебник Haskell когда-либо советовал вам забыть и никогда не использовать.