Как гарантировать ссылочную прозрачность в приложениях F#?


Итак, я пытаюсь изучить FP, и я пытаюсь разобраться в ссылочной прозрачности и побочных эффектах.

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

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

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

Я также узнал, что не-чистые языки FP, такие как Scala или F#, не могут гарантировать ссылочную прозрачность:

Возможность обеспечения ссылочной прозрачности это в значительной степени несовместимо с целью Scala иметь систему классов/объектов, которая совместима с Java. - Источник

И что в не-чистом FP это зависит от программиста, чтобы обеспечить референтный прозрачность:

В нечистых языках, таких как ML, Scala или F#, программист должен обеспечить ссылочную прозрачность, и, конечно, в динамически типизированных языках, таких как Clojure или Scheme, нет статической системы типов для обеспечения ссылочной прозрачности. - Источник

Меня интересует F#, потому что у меня есть .Net-фон, поэтому мои следующие вопросы:

Что я могу сделать, чтобы гарантировать ссылочную прозрачность в приложениях F#, если это не так принудительно с помощью компилятора F#?

3 4

3 ответа:

Короткий ответ на этот вопрос заключается в том, что нет способа гарантировать ссылочную прозрачность в F#. Одним из больших преимуществ F# является то, что он имеет фантастическое взаимодействие с другими языками .NET, но недостатком этого, по сравнению с более изолированным языком, таким как Haskell, является наличие побочных эффектов, и вам придется иметь с ними дело.


Как вы на самом деле справляетесь с побочными эффектами в F# - это совершенно другой вопрос.

На самом деле вас ничто не остановит. от введения эффектов в систему типов в F# во многом так же, как вы могли бы в Haskell, хотя фактически вы "выбираете" этот подход, а не навязываете его вам.

Все, что вам действительно нужно, - это некоторая инфраструктура, подобная этой:

/// A value of type IO<'a> represents an action which, when performed (e.g. by calling the IO.run function), does some I/O which results in a value of type 'a.
type IO<'a> = 
    private 
    |Return of 'a
    |Delay of (unit -> 'a)

/// Pure IO Functions
module IO =   
    /// Runs the IO actions and evaluates the result
    let run io =
        match io with
        |Return a -> a            
        |Delay (a) -> a()

    /// Return a value as an IO action
    let return' x = Return x

    /// Creates an IO action from an effectful computation, this simply takes a side effecting function and brings it into IO
    let fromEffectful f = Delay (f)

    /// Monadic bind for IO action, this is used to combine and sequence IO actions
    let bind x f =
        match x with
        |Return a -> f a
        |Delay (g) -> Delay (fun _ -> run << f <| g())

return приносит значение в пределах IO.

fromEffectful берет побочную функцию unit -> 'a и вносит ее в IO.

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

run запускает IO для выполнения всех вложенных эффектов. Это похоже на unsafePerformIO в Хаскелле.

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


Еще один стоящий вопрос: полезно ли это в F#?

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

Когда вы работаете в монаде Ио в Хаскелле, вы (как правило) гарантируете что-то о последовательной природе Ио и гарантируете, что одна часть ИО выполняется раньше другой. Вы также гарантируете что-то о том, как часто и когда могут произойти эффекты.

Один пример, который я люблю представлять в F#, это один:

let randomSeq = Seq.init 4 (fun _ -> rnd.Next())
let sortedSeq = Seq.sort randomSeq

printfn "Sorted: %A" sortedSeq
printfn "Random: %A" randomSeq
На первый взгляд может показаться, что этот код генерирует последовательность, сортирует ту же последовательность и затем печатает отсортированную и несортированную версии. Он генерирует две последовательности, одна из которых сортируется, а другая нет. они могут иметь и почти наверняка имеют совершенно разные значения. Это прямое следствие сочетания побочных эффектов и ленивой оценки без референтной прозрачности. Вы можете вернуть себе некоторый контроль, используя Что предотвращает повторную оценку, но все же не дает вам контроля над тем, когда и в каком порядке происходят эффекты.

Напротив, когда вы работаете с нетерпеливо оцениваемыми структурами данных, последствия, как правило, менее коварны, поэтому я думаю, что требованиедля явных эффектов в F# значительно уменьшено по сравнению с Haskell.


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

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

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


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

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

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

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

Чтобы ответить на некоторые конкретные пункты в кавычках:

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

Я согласен, что невозможно сделать их "безопасными" (если под безопасностью мы подразумеваем отсутствие побочных эффектов), но вы можете сделать их безопаснее, удалив Некоторые побочные эффекты.

Оставляя один вид эффекта, часто достаточно имитировать тот самый эффект, который вы только что пытались удалить.

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

Я также узнал, что не-чистые языки FP, такие как Scala или F#, не могут гарантировать ссылочную прозрачность:

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

Как подтвердил Марк Seemann в комментариях "Ничто в F# не может гарантировать ссылочную прозрачность. Об этом должен думать программист."

Я провел поиск в интернете и обнаружил, что "дисциплина - ваш лучший друг" и некоторые рекомендации, чтобы попытаться сохранить уровень ссылочной прозрачности в ваших приложениях F# как можно выше:

  • Не используйте изменяемые циклы for или while, ключевые слова ref и т. д.
  • придерживайтесь чисто неизменяемые структуры данных (различаемое объединение, список, кортеж, карта и т. д.).
  • Если вам нужно сделать IO в какой-то момент, создайте свою программу так, чтобы они были отделены от вашего чисто функционального кода. Не забывайте, что функциональное программирование - это ограничение и изоляция побочных эффектов.
  • алгебраические типы данных (ADT), известные как "дискриминируемые объединения" вместо объектов.
  • Учимся любить лень.
  • обнимая монады.