簡體   English   中英

“世界”在函數式編程世界中意味着什么?

[英]What does the “world” mean in functional programming world?

我已經潛入函數式編程超過3年了,我一直在閱讀和理解函數式編程的許多文章和方面。

但我經常偶然發現很多關於副作用計算中“世界”的文章,並且還在IO monad樣本中攜帶和復制“世界”。 在這種情況下,“世界”意味着什么? 這在所有副作用計算環境中是否與“世界”相同,還是僅在IO monads中應用?

關於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中的示例代碼。

“世界”只是一個捕捉“世界狀態”的抽象概念,即當前計算之外的一切狀態。

拿這個I / O功能,例如:

write : Filename -> String -> ()

這是無效的,因為它會通過副作用更改文件(其內容是世界狀態的一部分)。 但是,如果我們將世界建模為顯式對象,我們可以提供此功能:

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

這將獲取當前世界並在功能上生成一個“新”文件,並修改文件,然后您可以將其傳遞給連續調用。 世界本身只是一種抽象類型,除了通過read類的相應函數之外,沒有辦法直接窺視它。

現在,上述界面存在一個問題:沒有進一步的限制,它將允許程序“復制”世界。 例如:

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

你已經使用了同一個世界w兩次,產生兩個不同的未來世界。 顯然,作為物理I / O的模型,這沒有任何意義。 為了防止這樣的例子,需要一種更加花哨的類型系統,以確保線性處理世界,即從未使用過兩次。 Clean的語言基於這種想法的變化。

或者,您可以封裝世界,使其永遠不會變得明確,從而無法通過構造進行復制。 這就是I / O monad所實現的 - 它可以被認為是一個狀態monad,其狀態是世界,它隱含地通過monadic動作。

“世界”是一種將命令式編程嵌入到純函數式語言中的概念。

正如您當然所知,純函數式編程要求函數的結果完全依賴於參數的值。 因此,假設我們想將典型的getLine操作表示為純函數。 有兩個明顯的問題:

  1. 每次使用相同的參數調用getLine都會產生不同的結果(在本例中沒有參數)。
  2. getLine具有消耗流的某些部分的副作用。 如果您的程序使用getLine ,則(a)每次調用它必須使用輸入的不同部分,(b)程序輸入的每個部分必須由某些調用使用。 (你不能兩次調用getLine讀取相同的輸入行兩次,除非該行在輸入中出現兩次;你不能讓程序隨機跳過一行輸入。)

所以getLine只是不能成為一個函數,對吧? 嗯,不是那么快,我們可以做一些技巧:

  1. 多次調用getLine可以返回不同的結果。 為了使其與純函數行為兼容,這意味着純函數式getLine可以接受一個參數: getLine :: W -> String 然后我們可以通過規定每個調用必須使用W參數的不同值來調和每個調用的不同結果的概念。 您可以想象W代表輸入流的狀態。
  2. 必須以某種確定的順序執行對getLine多次調用,並且每次調用必須消耗前一次調用遺留的輸入。 更改:給getLine類型W -> (String, W) ,並禁止多次使用W值的程序(我們可以在編譯時檢查)。 現在要在程序中多次使用getLine ,您必須注意將先前調用的W結果提供給后續調用。

只要您可以保證W s不被重用,您就可以使用這種技術將任何(單線程)命令式程序轉換為純粹的功能性程序。 您甚至不需要為W類型提供任何實際的內存中對象 - 您只需鍵入 - 檢查您的程序並進行分析以證明每個W僅使用一次,然后發出不涉及任何內容的代碼那種。

因此,“世界”就是這個想法,但總體上涵蓋了所有必要的操作,而不僅僅是getLine


現在已經解釋了所有這些,你可能想知道你是否更好地了解這一點。 我的意見不是,你不是。 看,IMO,整個“傳遞世界”的想法就像monad教程之類的東西,其中有太多的Haskell程序員選擇以實際上沒有的方式“有用”。

“傳遞世界”通常作為“解釋”來幫助新手理解Haskell IO。 但問題在於:(a)對於許多人來說,這是一個非常奇特的概念(“你的意思是我要通過整個世界的狀態 ?”),(b)非常抽象(很多人無法理解你的程序幾乎每個函數都會有一個未使用的虛擬參數,既不會出現在源代碼中也不會出現在目標代碼中),而且(c)不是最簡單,最實用的解釋。

Haskell I / O,恕我直言的最簡單,最實用的解釋如下:

  1. Haskell純粹是功能性的,所以像getLine這樣的東西不能成為函數。
  2. 但是Haskell有像getLine這樣的東西。 這意味着那些東西不是一種功能。 我們稱之為行動
  3. Haskell允許您將操作視為值。 您可以擁有產生動作的函數(例如, putStrLn :: String -> IO () ),接受動作作為參數的函數(例如, (>>) :: IO a -> IO b -> IO b)等。
  4. 然而,Haskell沒有執行動作的功能。 不能有execute :: IO a -> a因為它不是真正的函數。
  5. 哈斯克爾有內置的功能來撰寫動作:使復合動作進行簡單的操作。 使用基本操作和動作組合器,您可以將任何命令性程序描述為操作。
  6. Haskell編譯器知道如何將操作轉換為可執行的本機代碼。 因此,您通過根據子動作編寫main :: IO ()動作來編寫可執行的Haskell程序。

傳遞代表“世界”的值是在純聲明性編程中制作用於執行IO(和其他副作用)的純模型的一種方法。

純聲明(不僅僅是功能)編程的“問題”是顯而易見的。 純聲明性編程提供了一種計算模型 這些模型可以表達任何可能的計算,但在現實世界中,我們使用程序讓計算機做一些不是理論意義上的計算:輸入,渲染到顯示,讀取和寫入存儲,使用網絡,控制機器人等等您可以直接將所有這些程序建模為計算(例如,如果輸入是計算,應該將哪個輸出寫入文件),但實際與程序外部事物的交互不是純模型的一部分。

這也是命令式編程的真實情況。 作為C編程語言的計算“模型”無法寫入文件,從鍵盤讀取或任何東西。 但是命令式編程中的解決方案是微不足道的。 在命令式模型中執行計算是執行指令序列,並且每個指令實際執行的操作取決於程序執行時的整個環境。 因此,您只需提供執行IO操作的“魔術”指令即可。 由於命令式程序員習慣於在操作上考慮他們的程序1 ,這非常適合他們已經在做的事情。

但是在所有純粹的計算模型中,給定的計算單位(函數,謂詞等)所做的只應該依賴於它的輸入,而不是每次都可能不同的任意環境。 因此,不僅可以執行IO操作,而且還可以實現依賴於程序外的Universe的計算。

然而,解決方案的想法相當簡單。 您構建了一個模型,用於說明IO操作如何在整個純計算模型中工作。 那么適用於純模型的所有原理和理論也將適用於模擬IO的部分。 然后,在語言或庫實現中(因為它不能在語言本身中表達),您可以將IO模型的操作與實際的IO操作聯系起來。

這使我們傳遞代表​​世界的價值。 例如,Mercury中的“hello world”程序如下所示:

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

該程序被賦予InitialWorld ,類型為io的值,代表程序外的整個Universe。 它將這個世界傳遞給了print ,它將它回饋給了TmpWorld ,這個世界就像是InitialWorld但是“Hello world!” 已被打印到終端,並且在此期間發生的任何其他事件,因為InitialWorld被傳遞到main也被合並。 然后TmpWorld傳遞給nl ,它返回了FinalWorld (一個非常像TmpWorld的世界,但它結合了換行符的打印,以及TmpWorld任何其他效果)。 FinalWorld是世界的最終狀態傳遞出的main回操作系統。

當然,我們並沒有真正將整個宇宙作為一個價值傳遞給程序。 在底層實現中,通常根本沒有類型io的值,因為沒有對實際傳遞有用的信息; 它都存在於程序之外。 但是使用我們傳遞io值的模型允許我們編程,好像整個Universe是受其影響的每個操作的輸入和輸出(並因此看到任何接受輸入和輸出io參數的操作) 不能受外界影響)。

事實上,通常你甚至不會想到那些做IO的程序就好像它們在宇宙中傳播一樣。 在真正的Mercury代碼中,你使用“狀態變量”語法糖,並像這樣寫上面的程序:

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

感嘆號語法表示!IO實際上代表兩個參數IO_XIO_Y ,其中XY部分由編譯器自動填充,使得狀態變量按照它們的順序“穿線”目標。書面。 這不僅僅適用於IO btw的上下文,狀態變量在Mercury中是非常方便的語法糖。

因此,程序員實際上傾向於將此視為一系列步驟(取決於並影響外部狀態),這些步驟按寫入順序執行。 !IO幾乎成為一個神奇的標記,只標記這適用的調用。

在Haskell中,IO的純模型是monad,“hello world”程序如下所示:

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

解釋IO monad的一種方法類似於State monad; 它會自動穿過狀態值,monad中的每個值都可以依賴或影響這個狀態。 只有在IO的情況下,被線程化的狀態才是整個Universe,就像Mercury程序一樣。 使用Mercury的狀態變量和Haskell的表示法,這兩種方法看起來非常相似,“world”以一種尊重源代碼中調用的順序的方式自動穿過,但仍然有IO動作明確標記。

正如在sacundim的回答中所解釋的那樣, Haskell的IO monad 解釋為IO-y計算模型的另一種方法是想象putStrLn "Hello world!" 實際上並不是一個計算“宇宙”需要被線程化的計算,而是putStrLn "Hello World!" 它本身就是一個描述可以采取的IO動作的數據結構。 基於這種理解, IO monad中正在執行的程序是使用純Haskell程序在運行時生成命令式程序。 在純哈斯克爾沒有辦法實際執行該程序,但由於main是類型的IO () main本身求這樣的計划,我們只知道操作性的Haskell的運行時將執行的main程序。

由於我們將這些純粹的IO 模型與實際的外部交互聯系起來,我們需要謹慎一點。 我們編程好像整個宇宙是一個值,我們可以傳遞與其他值相同的值。 但是其他值可以傳遞給多個不同的調用,存儲在多態容器中,以及許多其他對實際Universe沒有任何意義的事情。 因此,我們需要一些限制,阻止我們對模型中的“世界”做任何事情,這與實際可以對現實世界做出的事情無關。

Mercury采用的方法是使用獨特的模式來強​​制io值保持唯一。 這就是輸入和輸出世界分別被聲明為io::diio::uo ; 它是一個簡寫,用於聲明第一個參數的類型是io ,它的模式是di (“破壞性輸入”的縮寫),而第二個參數的類型是io ,其模式是uo (“唯一輸出”的縮寫) 。 由於io是一個抽象類型,因此無法構造新的類型,因此滿足唯一性要求的唯一方法是始終將io值傳遞給最多一個調用,這也必須返回一個唯一的io值,然后從你調用的最后一個東西輸出最終的io值。

在Haskell中采用的方法是使用monad接口來允許IO monad中的值從純數據和其他IO值構造,但不暴露IO值上的任何函數,這些函數允許您從中提取純數據。 IO monad。 這意味着只有包含在mainIO值才能執行任何操作,並且這些操作必須正確排序。

之前我曾提到,用純語言編寫IO程序員仍傾向於在操作上思考他們的大多數IO。 那么,如果我們只是按照命令式程序員的方式來思考它,為什么要為IO提出一個純粹的模型呢? 最大的優點是,現在所有適用於所有語言的理論/代碼/適用於IO代碼。

例如,在Mercury中, fold的等價物逐個元素地fold處理以構建累加器值,這意味着fold將一些任意類型的輸入/輸出變量對作為累加器(這是一個非常常見的模式) Mercury標准庫,這就是為什么我說狀態變量語法在其他情況下通常非常方便而不是IO)。 由於“世界”在Mercury程序中明確地顯示為類型io的值,因此可以使用io值作為累加器! 在Mercury中打印字符串列表就像foldl(print, MyStrings, !IO)一樣簡單。 類似地,在Haskell中,通用monad / functor代碼在IO值上工作得很好。 我們得到了許多“高階”IO操作,這些IO操作必須在一種語言中重新實現IO,該語言通過一些完全特殊的機制來處理IO。

此外,由於我們避免通過IO破壞純模型,即使在存在IO的情況下,對計算模型也適用的理論仍然是正確的。 這使得程序員和程序分析工具的推理不必考慮是否可能涉及IO。 例如,在Scala等語言中,即使很多“普通”代碼實際上是純粹的,但是對純代碼起作用的優化和實現技術通常也不適用,因為編譯器必須假定每個調用都可能包含IO或其他影響。


1在程序操作上思考程序意味着在執行計算機時將執行的操作。

我想我們應該首先閱讀關於這個主題的事情是解決尷尬的小隊 (我沒有這樣做,我就后悔了。)作者實際上描述的GHC的內部表示IOworld -> (a,world)為“一個黑客位的”。 我認為這種“黑客”意味着一種無辜的謊言。 我認為這里有兩種謊言:

  1. GHC假裝“世界”可以通過某種變量來表示。
  2. 類型world -> (a,world)基本上說如果我們能以某種方式實例化世界,那么我們世界的“下一個狀態”在功能上由計算機上運行的一些小程序決定。 由於這顯然無法實現,原語(當然)實現為具有副作用的函數,忽略了無意義的“世界”參數,就像在大多數其他語言中一樣。

作者在兩個基礎上為這個“黑客”辯護:

  1. 通過將IO視為類型world -> (a,world)的薄包裝world -> (a,world) ,GHC可以為IO代碼重用許多優化,因此這種設計非常實用且經濟。
  2. 如果編譯器滿足某些屬性,則可以證明如上實現的IO計算的操作語義是合理的。 引用本文作為證明。

問題(我想在這里問一下,但是你先問過它,原諒我在這里寫)是在標准的'懶惰IO'函數的存在中,我不再確定GHC的操作語義是否仍然合理。

標准的“惰性IO”函數(如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程序的精確評估順序幾乎是不可預測的 - 我們得到的是一些幾乎不受約束和不同步的並發競爭,用於某些寶貴的系統資源,如文件句柄。 當然, Ariola&Sabry從未考慮過這種操作。 所以我在這方面不同意安德烈亞斯 - 即使我們將自己限制在標准庫的限制范圍內 ,IO monad也沒有真正正確地處理世界(這就是為什么有些人說懶惰的IO很糟糕)。

世界意味着 - 物理的,現實的世界。 (請注意,只有一個。)

通過忽略限制在CPU和內存中的物理過程,可以對每個函數進行分類:

  1. 那些在物理世界中沒有影響的東西(除了短暫的,在CPU和RAM中幾乎不可觀察的影響)
  2. 那些確實有可觀察到的影響。 例如:在打印機上打印一些東西,通過網絡電纜發送電子,發射火箭或移動磁頭。

區別是有點人為的,因為在現實運行即使是最純粹的Haskell程序也會產生可觀察到的影響,例如:你的CPU變熱,導致風扇打開。

基本上你編寫的每個程序都可以分為兩部分(在FP中,在命令式/ OO世界中沒有這樣的區別)。

  1. 核心/純部分:這是您應用程序的實際邏輯/算法,用於解決構建應用程序的問題。 (95%的應用程序今天缺少這一部分,因為它們只是亂七八糟的API調用,並且人們開始稱自己為程序員)例如:在圖像處理工具中,對圖像應用各種效果的算法屬於這個核心部分。 所以在FP中,你使用像純度等FP這樣的概念來構建這個核心部分。你可以構建你的函數來獲取輸入和返回結果,並且在你的應用程序的這一部分中沒有任何突變。

  2. 外層部分:現在讓我們說你已經完成了圖像處理工具的核心部分,並通過使用各種輸入調用函數並檢查輸出來測試算法,但這不是你可以發布的東西,用戶應該如何使用這個核心部分,沒有面子,它只是一堆功能。 現在要從最終用戶的角度使這個核心usable ,你需要構建某種UI,從磁盤讀取文件的方式,可能使用一些嵌入式數據庫來存儲用戶首選項,列表繼續。 這種與其他各種東西的交互,這不是你的應用程序的核心概念,但仍然需要使它可用,在FP中被稱為world

練習:考慮一下你之前構建的任何應用程序,並嘗試將其分為上面提到的兩個部分,希望這會使事情變得更清晰。

例如,世界指的是與現實世界的互動/具有副作用

fprintf file "hello world"

這有副作用 - 該文件已添加"hello world"

這與純粹的功能代碼相反

let add a b = a + b

沒有副作用

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM