Кодирование без ошибок с замыканиями (в не чистой виртуальной среде) - PullRequest
2 голосов
/ 14 января 2010

В последние дни я изо всех сил пытаюсь понять замыкания. Я очень большой поклонник C #, поэтому мой основной тестовый стенд - это язык, поэтому я хотел бы узнать о его поддержке закрытия. Изучая и экспериментируя, я обнаружил, что многие люди, пытаясь вести блог о замыканиях, делают это, следуя совершенно неправильному направлению. Они проецируют определенное ошибочное использование замыканий, таких как хорошо известное выражение for, и пытаются это объяснить. Вместо этого я хотел бы видеть математический подход (первоклассный гражданин, свободные / связанные переменные, лямбды и т. Д.). Однако это заставляет меня задуматься о том, что я хотел бы знать, какие ошибки могут возникать при кодировании без учета замыканий.

Кроме того, все ли языки имеют одинаковую интерпретацию математической конструкции замыканий?

У меня не было ни курса FP, ни продвинутых языков программирования в универе. Но я знаю роль побочных эффектов в процедурном коде и их отсутствие в чисто виртуальных языках. Являются ли замыкания в C # просто уловкой? Какие (например) замыкания F # имеют больше замыканий C #?

Ответы [ 4 ]

5 голосов
/ 15 января 2010

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

Как реализовано в .NET (F # и C #), делегат на самом деле является ссылкой на некоторый метод в некотором классе. Причина в том, что для делегата , созданного с использованием синтаксиса лямбда-функции *1017*, может потребоваться некоторое состояние:

Func<int, int> CreateAdder(int num) {
  return arg => arg + num;
}

Возвращенный делегат ссылается на некоторый (неназванный) объект, который хранит значение num, а также тело лямбда-функции . Итак, что такое замыкание ? Закрытие - это объект, который сохраняет состояние, необходимое для запуска значения функции (или delgate ). В данном случае это безымянный объект, на который ссылается делегат и который сохраняет значение num.

Вы также упомянули свободные и связанные переменные. Если вы посмотрите на лямбда-функцию в приведенном выше примере, она работает с двумя переменными. Переменная arg объявлена ​​как часть лямбда-функции . Это будет называться связанной переменной лямбда-функции ), потому что она объявлена ​​как часть лямбда-функции. Переменная num будет называться свободной переменной в лямбда-функции , поскольку она используется (но не объявляется!) В области действия лямбда-функции . Это происходит из внешней области видимости (в данном случае, из объявления метода).

Закрытие должно захватывать все свободные переменные в лямбда-функции . Это означает, что все переменные, которые используются внутри тела, но объявлены где-то еще, перехвачены. Однако компилятор C # не просто копирует текущее значение. Он превращает его в изменяемое поле, так что к нему можно получить доступ (и также мутировать) из всех функций, которые могут получить к нему доступ (и это также место, где оно становится сложным). Это было бы темой для длинного поста в блоге, но вот краткий пример, который вы можете использовать, чтобы поэкспериментировать с этим (используя Tuple из .NET 4.0, если вы используете VS 2008, вы можете получить реализацию C # здесь, в Chapter03 / FunctionalCSharp ):

Tuple<Func<int>, Action<int>> CreateReaderAndWriter(int initial) {
   int state = initial;
   return Tuple.Create( (() => state),
                        (newState => { state = newState; }) );
}

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

Дон Сайм из MSR предложил добавить поддержку замыканий непосредственно в .NET. Это немного академично, но может помочь немного прояснить ситуацию.

4 голосов
/ 14 января 2010

Понятие замыкания довольно согласованно для разных языков, хотя в императивных языках существуют некоторые разногласия относительно того, как должны обрабатываться такие конструкции, как continue, break и return внутри замыкания (например, некоторые предложения для добавления замыканий в Java ведут себя иначе, чем в C #). Основная тонкость, которая привлекает внимание людей, заключается в том, что в не чистых языках замыкания «закрывают» привязки переменных, а не значения, что означает, что такие вещи, как область видимости переменных, очень важны (и именно здесь неожиданные трудности возникают в примерах цикла for, поскольку область видимости переменной цикла не всегда соответствует ожидаемой).

Поведение F # очень похоже на поведение C #, но есть несколько вещей, которые делают замыкания немного приятнее в F #. Во-первых, хотя F # нечист, мутация не рекомендуется, поэтому сложнее написать замыкание, которое непреднамеренно закрывает переменную, которая впоследствии изменяется таким образом, что нарушает ожидания. В частности, компилятор F # не позволяет использовать обычную изменяемую привязку внутри замыкания - сообщение об ошибке компилятора предполагает, что вы либо делаете привязку неизменной, либо используете явную ссылочную ячейку (тип 'a ref в F #), если вы на самом деле намереваться закрыть привязку, которая может быть видоизменена. Это заставляет пользователя тщательно обдумывать, чего он хочет достичь.

1 голос
/ 14 января 2010

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

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

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

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

0 голосов
/ 14 января 2010

Вы читали дискурс Мартина Фаулера на эту тему? Это, кажется, покрывает ваши проблемы и исходит из довольно авторитетной цифры: http://martinfowler.com/bliki/Closure.html

...