Подтвердить что ты не робот

Что означает "мир" в мире функционального программирования?

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

Но я часто натыкался на многие статьи о "мире" в вычислениях побочных эффектов, а также переносил и копировал "мир" в образцах монады IO. Что означает "мир" в этом контексте? Является ли это одним и тем же "миром" во всех контекстах вычисления побочных эффектов или применяется только в моноданных IO?

Также в документации и других статьях о Haskell много раз упоминается "мир" .

Некоторые упоминания об этом "мире": http://channel9.msdn.com/Shows/Going+Deep/Erik-Meijer-Functional-Programming

и это: http://www.infoq.com/presentations/Taming-Effect-Simon-Peyton-Jones

Я ожидаю образец, а не просто объяснение мировой концепции. Я приветствую образец кода в Haskell, F #, Scala, Scheme.

4b9b3361

Ответ 1

"Мир" - это просто абстрактное понятие, которое фиксирует "состояние мира", т.е. состояние всего за пределами текущего вычисления.

Возьмите эту функцию ввода-вывода, например:

write : Filename -> String -> ()

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

write : World -> Filename -> String -> World

Это занимает текущий мир и функционально создает "новый", с измененным файлом, который затем можно передать на последовательные вызовы. Сам Мир является абстрактным типом, нет возможности заглянуть в него напрямую, за исключением соответствующих функций, таких как read.

Теперь есть одна проблема с указанным выше интерфейсом: без дальнейших ограничений это позволит программе "дублировать" мир. Например:

w1 = write w "file" "yes"
w2 = write w "file" "no"

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

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

Ответ 2

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

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

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

Итак, getLine просто не может быть функцией, правильно? Ну, не так быстро, есть некоторые трюки, которые мы могли бы сделать:

  • Несколько вызовов на getLine могут возвращать разные результаты. Чтобы сделать это совместимым с чисто функциональным поведением, это означает, что чисто функциональный getLine может принять аргумент: getLine :: W -> String. Затем мы можем согласовать идею разных результатов по каждому вызову, указав, что каждый вызов должен быть выполнен с другим значением для аргумента W. Вы можете себе представить, что W представляет состояние входного потока.
  • Несколько вызовов на getLine должны выполняться в определенном порядке, и каждый из них должен потреблять вход, который был оставлен после предыдущего вызова. Изменить: дать getLine тип W -> (String, W) и запретить программам использовать значение W более одного раза (что мы можем проверить при компиляции). Теперь, чтобы использовать getLine более одного раза в вашей программе, вы должны позаботиться о том, чтобы передать результат предыдущего вызова W на следующий вызов.

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

Итак, "мир" - это просто эта идея, но обобщенная для покрытия всех императивных операций, а не только getLine.


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

"Прохождение мира вокруг" обычно предлагается как "объяснение", чтобы помочь новичкам понять Haskell IO. Но проблема в том, что (а) это очень экзотическая концепция для многих людей обернуть свои головы ("что вы имеете в виду, я собираюсь пройти состояние всего мира вокруг?)), (б) очень абстрактно (многие люди не могут обойти мысль о том, что почти каждая функция вашей программы будет иметь неиспользуемый фиктивный параметр, который не отображается ни в исходном коде, ни в объектном коде), и (c) не самое легкое и практичное объяснение.

Самое простое и практичное объяснение ввода/вывода Haskell, IMHO, выглядит следующим образом:

  • Haskell является чисто функциональным, поэтому такие вещи, как getLine, не могут быть функциями.
  • Но у Haskell есть такие вещи, как getLine. Это означает, что это нечто другое, что не является функцией. Мы называем их действиями.
  • Haskell позволяет рассматривать действия как значения. У вас могут быть функции, которые производят действия (например, putStrLn :: String -> IO ()), функции, которые принимают действия в качестве аргументов (например, (>>) :: IO a -> IO b -> IO b) и т.д.
  • Однако у Haskell нет функции, которая выполняет действие. Не может быть execute :: IO a -> a, потому что это не будет истинная функция.
  • Haskell имеет встроенные функции для составления действий: сделать сложные действия из простых действий. Используя основные действия и комбинаторы действий, вы можете описать любую императивную программу как действие.
  • Компиляторы Haskell знают, как перевести действия в исполняемый собственный код. Таким образом, вы пишете исполняемую программу Haskell, записывая действие main :: IO () в терминах подделок.

Ответ 3

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

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

Это действительно верно и для императивного программирования. "Модель" вычисления, которая является языком программирования C, не дает возможности писать в файлы, читать с клавиатуры или что-то еще. Но решение в императивном программировании тривиально. Выполнение вычисления в императивной модели выполняет последовательность инструкций, и то, что каждая команда фактически выполняет, зависит от всей среды программы в момент ее выполнения. Таким образом, вы можете просто предоставить "магические" инструкции, которые выполняют ваши действия IO при их исполнении. И поскольку императивные программисты привыкли думать о своих программах оперативно 1 это очень естественно соответствует тому, что они уже делают.

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

Идея решения довольно проста. Вы строите модель для того, как действия IO работают во всей чистой модели вычисления. Тогда все принципы и теории, которые применимы к чистой модели в целом, будут также применяться к той части, которая моделирует IO. Затем, в рамках реализации языка или библиотеки (поскольку он не выражается в самом языке), вы подключаете манипуляции с моделью ввода-вывода к действительным действиям IO.

Это приводит нас к обходу ценности, представляющей мир. Например, программа "hello world" в Mercury выглядит так:

:- pred main(io::di, io::uo) is det.
main(InitialWorld, FinalWorld) :-
    print("Hello world!", InitialWorld, TmpWorld),
    nl(TmpWorld, FinalWorld).

Программа предоставляется InitialWorld, значение в типе io, которое представляет весь юниверс вне программы. Он переносит этот мир на print, который возвращает его TmpWorld, мир, похожий на InitialWorld, но в котором "Hello world!" был напечатан на терминале, и все, что произошло за это время, так как InitialWorld был передан в main. Затем он передает TmpWorld в nl, что возвращает FinalWorld (мир, который очень похож на TmpWorld, но включает печать новой строки и любые другие эффекты, которые произошли за это время). FinalWorld - это конечное состояние мира, переданное из main обратно в операционную систему.

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

И на самом деле, как правило, вы даже не подумали бы о программах, которые выполняют IO, как будто они передают вселенную. В реальном коде Mercury вы должны использовать синтаксический сахар "переменная состояния" и написать вышеприведенную программу следующим образом:

:- pred main(io::di, io::uo) is det.
main(!IO) :-
    print("Hello world!", !IO),
    nl(!IO).

Синтаксис восклицательного знака означает, что !IO действительно означает два аргумента, IO_X и IO_Y, где части X и Y автоматически заполняются компилятором, так что переменная состояния "пронизывающий" через цели в том порядке, в котором они написаны. Это не просто полезно в контексте IO btw, переменные состояния - это действительно удобный синтаксический сахар, который есть в Mercury.

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

В Haskell чистая модель для IO является монадой, а программа "hello world" выглядит так:

main :: IO ()
main = putStrLn "Hello world!"

Один способ интерпретировать монаду io аналогичен монаде State; он автоматически передает значение состояния через, и каждое значение в монаде может зависеть от этого состояния или влиять на него. Только в случае io состояние, на которое выполняется поток, представляет собой всю вселенную, как в программе Mercury. Поскольку переменные состояния Mercury и обозначения Haskell обозначают друг друга, оба подхода в конечном итоге выглядят очень похожими, причем "мир" автоматически пронизывается таким образом, который учитывает порядок, в котором вызовы были записаны в исходном коде; = но все еще имеет io явно выделенные действия.

Как хорошо объясняется в ответе sacundim, другой способ интерпретировать Haskell io monad как модель для вычислений IO-y состоит в том, чтобы представить, что putStrLn "Hello world!" на самом деле не является вычислением, посредством которого "вселенная" должен быть потоковым, а скорее, что putStrLn "Hello world!" сам является структурой данных, описывающей действие IO, которое можно было бы предпринять. При таком понимании, какие программы в монаде io делают, используются чистые программы Haskell для генерации во время выполнения настоятельной программы. В чистом Haskell нет способа фактически выполнить эту программу, но поскольку main имеет тип IO () main сам оценивает такую ​​программу, и мы просто знаем, что операционная среда Haskell будет выполнять программу main.

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

Подход, использованный в Mercury, заключается в использовании уникальных режимов для обеспечения того, чтобы значение io оставалось уникальным. Именно поэтому мир ввода и вывода был объявлен как io::di и io::uo соответственно; это сокращение для объявления того, что тип первого параметра io, и его режим di (короткий для "деструктивного ввода" ), тогда как тип второго параметра io и его режим uo (сокращение от "уникального выхода" ). Поскольку io является абстрактным типом, нет способа построить новые, поэтому единственный способ удовлетворить требование уникальности - всегда передавать значение io не более одного вызова, что также должно дать вам уникальный io, а затем вывести окончательное значение io из последнего, что вы вызываете.

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

Я уже упоминал, что программисты, делающие io на чистом языке, все еще склонны думать оперативно о большей части своего IO. Итак, зачем идти на все эти неприятности, чтобы придумать чистую модель для ввода-вывода, если мы будем думать об этом так же, как это делают настоящие программисты? Большим преимуществом является то, что теперь все теории/код/​​все, что применимо ко всему языку, также относится к IO-коду.

Например, в Mercury эквивалент fold обрабатывает элемент-элемент по списку для создания значения аккумулятора, что означает, что fold принимает пару входных/выходных переменных какого-либо произвольного типа в качестве аккумулятора ( это очень распространенный шаблон в стандартной библиотеке Mercury, и поэтому я сказал, что синтаксис переменных состояния часто оказывается очень удобным в других контекстах, чем IO). Поскольку "мир" появляется в программах Mercury явно как значение в типе io, в качестве аккумулятора можно использовать значения io! Печать списка строк в Mercury так же просто, как foldl(print, MyStrings, !IO). Аналогично в Haskell, общий код монады/функтора отлично работает на значениях io. Мы получаем много операций ввода-вывода более высокого порядка, которые должны быть реализованы заново специализированными для ввода-вывода на языке, который обрабатывает IO с помощью некоторого полностью специального механизма.

Кроме того, поскольку мы избегаем разбить чистую модель на IO, теории, которые верны для вычислительной модели, остаются верными даже в присутствии IO. Это делает рассуждение программистом, а инструменты анализа программ не должны учитывать, может ли быть задействован IO. Например, в таких языках, как Scala, даже несмотря на то, что очень "нормальный" код на самом деле чист, оптимизаторы и методы реализации, которые работают с чистым кодом, обычно неприменимы, поскольку компилятор должен предположить, что каждый отдельный вызов может содержать IO или другие эффекты.


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

Ответ 4

Я думаю, что первое, что мы должны прочитать по этому вопросу, - "Отстрел неуклюжий отряд". (Я этого не делал, и я сожалею об этом). Фактически автор описывает внутреннее представление GHC IO как world -> (a,world) как "немного взломанного". Я думаю, что этот "взлом" означает некую невинную ложь. Я думаю, здесь есть два вида лжи:

  • GHC делает вид, что "мир" представлен некоторой переменной.
  • Тип world -> (a,world) в основном говорит, что если мы каким-то образом создадим экземпляр мира, то "следующее состояние" нашего мира функционально определяется некоторой небольшой программой, запущенной на компьютере. Так как это явно не реализуемо, примитивы (конечно) реализованы как функции с побочными эффектами, игнорируя бессмысленный "мировой" параметр, как и на большинстве других языков.

Автор защищает этот "хак" на двух основаниях:

  • Рассматривая IO как тонкую упаковку типа world -> (a,world), GHC может повторно использовать много оптимизаций для кода ввода-вывода, поэтому этот дизайн очень практичен и экономичен.
  • Оперативная семантика вычислений ввода-вывода, реализованная, как указано выше, может быть доказана, если компилятор удовлетворяет определенным свойствам. Эта статья приведена для доказательства этого.

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

Стандартные функции "ленивого ввода-вывода", такие как hGetContents внутренне вызывает unsafeInterleaveIO, что в свою очередь эквивалентно unsafeDupableInterleaveIO для программ с одним потоком.

unsafeDupableInterleaveIO :: IO a -> IO a
unsafeDupableInterleaveIO (IO m)
     = IO ( \ s -> let  r = case m s of (# _, res #) -> res
                   in  (# s, r #))

Притворившись, что эквациональное рассуждение все еще работает для таких программ (обратите внимание, что m - нечистая функция) и игнорируя конструктор, мы имеем unsafeDupableInterleaveIO m >>= f == > \world -> f (snd (m world)) world, который семантически будет иметь тот же эффект, что описанный Андреасом Росбергом выше: он "дублирует" мир. Так как наш мир не может быть продублирован таким образом, а точный порядок оценки программы Haskell практически непредсказуем --- то, что мы получаем, - это почти безусловный и несинхронизированный гоночный турнир w390 для некоторых ценных системных ресурсов, таких как дескрипторы файлов. Этот вид операции, конечно, никогда не рассматривается в Ariola & Sabry. Поэтому я не согласен с Андреасом в этом отношении - монада IO действительно не вписывает мир должным образом, даже если мы ограничимся пределами лимита стандартной библиотеки (и именно поэтому некоторые люди говорят, что ленивый IO плохой).

Ответ 5

Мир означает именно это - физический, реальный мир. (Есть только один, заметьте.)

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

  • Те, которые не имеют эффектов в физическом мире (за исключением эфемерных, в основном ненаблюдаемых эффектов в ЦП и ОЗУ)
  • Те, которые имеют наблюдаемые эффекты. например: распечатать что-то на принтере, отправить электроны через сетевые кабели, запустить ракеты или переместить головки дисков.

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

Ответ 6

В принципе, каждая написанная вами программа может быть разделена на 2 части (в слове FP, в мире императива /OO такого различия нет).

  • Core/Pure part: Это ваша фактическая логика/алгоритм приложения, который используется для решения проблемы, для которой вы создали приложение. (95% приложений сегодня не имеют этой части, так как они просто беспорядок вызовов API с if/else sprinkled, и люди начинают называть себя программистами). Например: в инструменте обработки изображений алгоритм применения различных эффектов к изображению принадлежит эта основная часть. Таким образом, в FP вы создаете эту основную часть, используя концепции FP, такие как чистота и т.д. Вы создаете свою функцию, которая принимает результат ввода и возврата, и в этой части вашего приложения нет никакой мутации.

  • Часть внешнего слоя: теперь можно сказать, что вы завершили основную часть инструмента обработки изображений и протестировали алгоритмы, вызвав функцию с различными входами и проверив вывод, но это не то, что вы можете отправить, как пользователь должен использовать эту основную часть, нет лица, это всего лишь куча функций. Теперь, чтобы сделать это ядро ​​ usable с точки зрения конечного пользователя, вам нужно создать какой-то пользовательский интерфейс, способ чтения файлов с диска, может быть использована некоторая встроенная база данных для хранения пользовательских настроек, и список можно продолжить. Это взаимодействие с другими другими вещами, которое не является основной концепцией вашего приложения, но все еще необходимо для его использования, называется world в FP.

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

Ответ 7

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

fprintf file "hello world"

который имеет побочный эффект - файл добавил "hello world" к нему.

Это противоречит чисто функциональному коду, например

let add a b = a + b

который не имеет побочных эффектов