В чем разница между изменяемыми значениями и переопределением неизменяемых значений?

Я читал, что значения в F # неизменяемы. Однако я также сталкивался с концепцией переопределения определений значений, которые затмевают предыдущие. Чем оно отличается от изменяемого значения? Я спрашиваю об этом не только как о теоретической конструкции, но и о том, есть ли какие-либо советы о том, когда использовать изменяемые значения, а когда вместо этого переопределять выражения; или если кто-то может указать, что последнее не является идиоматическим f #.

Базовый пример переопределения:

let a = 1;;
a;; //1
let a = 2;;
a;; //2

Обновление 1:

Добавляя к ответам ниже, переопределение в Fsharp interactive на верхнем уровне разрешено только в разных окончаниях. Следующее также вызовет ошибку в fsi:

let a = 1
let a = 2;;

Error: Duplicate definition of value 'a'

С другой стороны, в привязках let допускается переопределение.

Обновление 2: практическая разница, замыкания не могут работать с изменяемыми переменными:

let f =
   let mutable a = 1
   let g () = a //error
   0  
f;;

Обновление 3:

Хотя я могу моделировать побочные эффекты с помощью ссылок, например:

let f =
   let  a = ref 1
   let g = a
   a:=2
   let x = !g  + !a
   printfn "x: %i" x //4

f;;

Я не вижу практической разницы между переопределением и использованием ключевого слова mutable, помимо разницы в использовании с замыканиями, например:

let f  =
   let a = 1
   let g  = a
   let a = 2
   let x = g + a
   printfn "x: %i" x //3

f;;

vs

let f =
   let mutable a = 1
   let g = a
   a <-2
   let x = g  + a
   printfn "x: %i" x //3
 f;;

Еще одна мысль: я не уверен, как работать с потоками, но (а) может ли другой поток изменить значение изменяемой переменной в привязке let и (б) может другой поток повторно связать / переопределить имя значения внутри пусть привязка. Я определенно что-то здесь упускаю.

Обновление 4: разница в последнем случае заключается в том, что мутация все равно будет происходить из вложенной области, тогда как переопределение / повторная привязка во вложенной области будет «затенять» определение из внешней области.

let f =
   let mutable a = 1
   let g = a
   if true then
      a <-2   
   let x = g  + a
   printfn "x: %i" x //3

f;;

vs

let f =
   let a = 1
   let g = a
   if true then
      let a = 2  
      printfn "a: %i" a   
   let x = g  + a
   printfn "x: %i" x //2
f;;

person user3056677    schedule 02.12.2013    source источник


Ответы (4)


«Я не уверен, что согласен с некоторыми из полученных ответов.

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

let TestShadowing() =
   let a = 1
   let a = 2
   a

Но важно понимать, что происходит не мутация, а слежка. Другими словами, значение «а» не переназначалось. Еще одна буква «а» была объявлена ​​со своим неизменным значением. Почему важно различие? Рассмотрим, что происходит, когда во внутреннем блоке затеняется 'a':

let TestShadowing2() =
   let a = 1
   printfn "a: %i" a
   if true then
      let a = 2
      printfn "a: %i" a
   printfn "a: %i" a

> TestShadowing2();;
a: 1
a: 2
a: 1

В этом случае вторая буква «а» только затеняет первую, в то время как вторая находится в области видимости. Как только он выходит за пределы области видимости, появляется первая буква «а».

Если вы этого не понимаете, это может привести к незначительным ошибкам!

Разъяснение в свете комментария Гая Кодера:

Поведение, которое я описываю выше, происходит, когда переопределение происходит в некоторой привязке let (т.е. в функциях TestShadowing () в моих примерах). Я бы сказал, что это наиболее распространенный сценарий на практике. Но, как говорит Гай, если вы переопределите на верхнем уровне, например:

module Demo =

   let a = 1
   let a = 2

вы действительно получите ошибку компилятора.

person Kit    schedule 03.12.2013
comment
Вам нужно добавить информацию о том, когда привязки не находятся в пределах let и выполняются в файле fs, что вызовет ошибку компилятора. - person Guy Coder; 03.12.2013

В частности, я не знаком с F #, но могу ответить на «теоретическую» часть.

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

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

person Ben    schedule 03.12.2013
comment
Отличный теоретический обзор ... кое-что о замыканиях, захватывающих имена, мне интересно, как это влияет на изменяемые значения в F #. Позвольте мне проверить - person user3056677; 03.12.2013

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

let f h = match h with h::t -> h

который вернет первый элемент, когда вы создадите новый h, который затеняет определение из аргумента.

Единственная причина, по которой redifintion работает, заключается в том, что вы можете ошибаться в fsi вот так

let one = 2;;
let one = 1;; //and fix the mistake

В скомпилированном коде F # это невозможно.

person John Palmer    schedule 02.12.2013
comment
Интересно, однако, что SML допускает затенение, что не то же самое, что мутация. - person Craig Stuntz; 02.12.2013
comment
Как и F #. Смотрите мой ответ. - person Kit; 03.12.2013

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

let f () =
   let a = 1
   let g () = a
   let a = 2
   g () + a

который возвращает 3, поскольку a в g относится к предыдущей привязке a, а последняя является отдельной. Вышеупомянутая программа полностью эквивалентна

let f () =
   let a = 1
   let g () = a
   let b = 2
   g () + b

где я последовательно переименовал вторую a и все ссылки на нее в b.

person Andreas Rossberg    schedule 03.12.2013
comment
Я могу проверить разницу при сравнении использования ref, но не с использованием ключевого слова mutable - person user3056677; 04.12.2013