簡體   English   中英

Haskell 和 Purity 中的 Monad

[英]Monads in Haskell and Purity

我的問題是 Haskell 中的 monad 是否真的保持了 Haskell 的純度,如果是的話,如何保持。 我經常讀到副作用是如何不純的,但有用的程序(例如 I/O)需要副作用。 在下一句話中,Haskell 對此的解決方案是 monads。 然后對 monad 進行了某種程度的解釋,但並沒有真正解釋它們如何解決副作用問題。

我已經看到了這個這個,我對答案的解釋實際上是我在自己的閱讀中得出的答案——IO monad 的“動作”不是 I/O 本身,而是在執行時執行我的對象/O。 但我突然想到,可以對任何代碼或任何編譯的可執行文件提出相同的論點。 你不能說C++程序只有在編譯后的代碼執行時才會產生副作用嗎? 所有的 C++ 都在 IO monad 中,所以 C++ 是純的? 我懷疑這是真的,但老實說,我不知道它在哪方面不是。 事實上,Moggi (sp?) 最初不是使用 monad 來模擬命令式程序的指稱語義嗎?

一些背景:我是 Haskell 和函數式編程的粉絲,我希望隨着我的學習繼續深入了解這兩者。 例如,我了解參考透明度的好處。 這個問題的動機是我是一名研究生,我將在編程語言課上做 2 個 1 小時的演講,一個特別涵蓋 Haskell,另一個涵蓋一般的函數式編程。 我懷疑班上的大多數人都不熟悉函數式編程,也許看過一些方案。 我希望能夠(合理地)清楚地解釋 monads 如何解決純度問題,而無需進入范疇論和 monads 的理論基礎,我沒有時間涵蓋,反正我不完全了解自己 - 當然不夠好呈現。

我想知道在這種情況下“純度”是否真的沒有明確定義?

很難在任何一個方向上做出結論性的爭論,因為“純”並不是特別明確的定義。 當然,某些東西使 Haskell 從根本上不同於其他語言,它與管理副作用和IO很大關系¹,但目前尚不清楚究竟是什么東西 給定一個具體的定義來引用,我們可以檢查它是否適用,但這並不容易:這樣的定義要么不符合每個人的期望,要么過於寬泛而無用。

那么是什么讓 Haskell 與眾不同呢? 在我看來,這是評估執行之間的分離。

與 λ 演算密切相關的基礎語言都是關於前者的。 您可以使用計算結果為其他表達式1 + 12表達式。 這里沒有副作用,不是因為它們被壓制或移除,而僅僅是因為它們首先沒有意義。 它們不是模型²的一部分,就像回溯搜索是 Java 模型的一部分一樣(與 Prolog 相對)。

如果我們只是堅持使用這種基本語言而沒有為IO添加額外的工具,我認為稱其為“純”是相當沒有爭議的。 作為 Mathematica 的替代品,它可能仍然有用。 您可以將程序編寫為表達式,然后在 REPL 處獲得對表達式求值的結果。 只不過是一個花哨的計算器,沒有人指責您在計算器中使用的表達式語言不純³!

但是,當然,這太局限了。 我們希望使用我們的語言來讀取文件、提供網頁、繪制圖片、控制機器人並與用戶交互。 因此,問題是如何在擴展我們的語言以完成我們想做的所有事情的同時,保留我們喜歡的關於評估表達式的一切。

我們想出的答案? IO 我們的類似計算器的語言可以評估的一種特殊類型的表達式,它對應於執行一些有效的操作。 至關重要的是,評估仍然像以前一樣工作,即使對於IO事情也是如此。 效果按照結果IO值指定的順序執行,而不是基於它的評估方式。 IO是我們用來將效果引入和管理到我們原本純粹的表達語言中的東西。

我認為這足以使將 Haskell 描述為“純”有意義。

腳注

¹ 請注意我是如何說IO而不是一般的 monad 的:monad 的概念對於許多與輸入和輸出無關的事情非常有用,並且IO類型必須不僅僅是一個 monad 才有用。 我覺得這兩者在共同的話語中聯系得太緊密了。

² 這就是unsafePerformIO如此不安全的原因:它打破了語言的核心抽象。 這與在 C 中使用特定寄存器相同:它既會導致奇怪的行為,又會阻止您的代碼可移植,因為它低於 C 的抽象級別。

³ 好吧,大多數情況下,只要我們忽略生成隨機數之類的事情。

例如,具有類型a -> IO b的函數在給定相同輸入時總是返回相同的 IO 操作; 它是純的,因為它不可能檢查環境,並且遵守純函數的所有通常規則。 這意味着,除其他外,編譯器可以將其所有常用優化規則應用於其類型中具有IO函數,因為它知道它們仍然是純函數。

現在,返回的 IO 操作可能會在 run時查看環境、讀取文件、修改全局狀態等等,一旦您運行一個操作,所有賭注都將關閉。 但是你不一定要運行一個動作; 您可以將其中的五個放入一個列表中,然后按照您創建它們的相反順序運行它們,或者如果您願意,可以根本不運行其中的一些; 如果 IO 操作在您創建它們時隱式地自行運行,則您無法執行此操作。

考慮這個愚蠢的程序:

main :: IO ()
main = do
  inputs <- take 5 . lines <$> getContents
  let [line1,line2,line3,line4,line5] = map print inputs
  line3
  line1
  line2
  line5

如果你運行這個,然后輸入 5 行,你會看到它們以不同的順序打印回給你,並且省略了一個,即使我們的 haskell 程序按照它們接收的順序運行map print 你不能用 C 的printf做到這一點,因為它在被調用時立即執行它的 IO; haskell 的版本只返回一個 IO 操作,您仍然可以將其作為一流的值進行操作並做任何您想做的事情。

我在這里看到兩個主要區別:

1)在haskell中,你可以做IO monad中沒有的事情。 為什么這很好? 因為如果你有一個函數definitelyDoesntLaunchNukes :: Int -> IO Int你不知道產生的 IO 動作不會發射核武器,你可能都知道。 cantLaunchNukes :: Int -> Int絕對不會發射任何核武器(除非在幾乎所有情況下都應該避免任何丑陋的黑客攻擊)。

2) 在haskell 中,這不僅僅是一個可愛的比喻:IO 操作是一流的值。 您可以將它們放在列表中,並根據需要將它們留在那里,除非它們以某種方式成為main動作的一部分,否則它們不會做任何事情。 C 最接近的是函數指針,它使用起來要麻煩得多。 在 C++(和大多數現代命令式語言)中,你有閉包,技術上可以用於這個目的,但很少 - 主要是因為 Haskell 是純的,而它們不是。

為什么這種區別在這里很重要? 那么,您將從哪里獲得其他 IO 操作/關閉? 可能是某些描述的功能/方法。 在不純的語言中,它們本身可能會產生副作用,從而使將它們隔離在這些語言中的嘗試毫無意義。


小說模式:主動

這是一個相當大的挑戰,我認為一個蟲洞可能會在鄰居的后院形成,但我設法從另一個現實中獲取了 Haskell I/O 實現的一部分:

class Kleisli k where
    infixr 1 >=>
    simple :: (a -> b) -> (a -> k b)
    (>=>)  :: (a -> k b) -> (b -> k c) -> a -> k c

instance Kleisli IO where
    simple = primSimpleIO
    (>=>)  = primPipeIO

primitive primSimpleIO :: (a -> b) -> (a -> IO b)
primitive primPipeIO   :: (a -> IO b) -> (b -> IO c) -> a -> IO c

回到我們稍微殘缺的現實(抱歉!),我使用了另一種形式的 Haskell I/O 來定義我們的 Haskell I/O 形式:

instance Monad IO where
    return x = simple (const x) ()
    m >>= k  = (const m >=> k) ()

它有效!

小說模式:離線


我的問題是 Haskell 中的 monad 是否真的保持了 Haskell 的純度,如果是的話,如何保持。

monadic interface本身並不保持純潔性——它只是一個接口,盡管它是一個多才多藝的接口。 正如我的小小說作品所示,這項工作還有其他可能的界面 - 只是它們在實踐中使用的方便程度的問題。

對於 Haskell I/O 的實現,保持純度的是所有相關實體,它們是:

  • IO , simple , (>=>)

或者:

  • IO , return , (>>=)

抽象的- 實現如何定義這些是私有的

否則,您將能夠像這樣設計“新奇事物”:

what_the_heck =
  do spare_world <- getWorld  -- how easy was that?
     launchMissiles           -- let's mess everything up,
     putWorld spare_world     -- because undoing it is just so easy :-D
     what_the_heck            -- that was fun; let's do it again!

(你不高興我們的現實沒有那么柔韌嗎?;-)

這種觀察擴展到ST (封裝狀態)和STM (並發)等類型及其管理員( runSTatomically等)。 對於像列表、 MaybeEither這樣的類型,它們在Haskell中的正統定義確保了純度。

所以當你看到一個接口——monadic、applicative 等——對於某些抽象類型時,通過保持其實現的私有性來保持純度; 避免以異常方式使用。

暫無
暫無

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

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