从命令式功能到纯功能性,然后再返回:Monads与范围内的延续
- 這則影片隨附此文章,沒有它不會有太大意義
上個月,我在Curry On會議上做了演講,該會議是與學術,編程語言會議ECOOP共同舉辦的新會議。 Curry On旨在彌合學術界之間的鴻溝。 我的學術興趣不包括編程語言,我認為編程語言是計算機科學的一門學科,與其他任何學科相比,它始終被高估和交付不足(可能是AI除外)。 我對算法比對抽象更感興趣,并且編程語言研究主要與后者有關。 但是,作為開發人員,我必須使用我選擇使用的編程語言提供的抽象,并且令我感到震驚的是,我注意到某些抽象從學術語言到主流的流行,在某些情況下使身體不好,主要引起疼痛。 舉個例子,我想想一想,與使用Haskell相比,現在Java中使用monad的人越來越多。
在我的演講中,我提出了命令式編程的核心抽象是阻塞線程。 一旦將其刪除,您將失去大多數其他命令式抽象,例如控制流和異常處理(要求它們在庫中重新實現),命令式語言帶來的大多數優勢包括事后調試,性能分析和自動背壓。 這也使代碼難于編寫和閱讀。 我認為,無論您是否使用monad減輕其痛苦,異步編程都是對命令式語言的厭惡。 異步和命令之間的不匹配是根本的。 一直以來,我們可以達到與monads一樣強大的抽象(如果不是更多的話),這自然是命令式語言的合適之選,與它們的結構和功能完美地結合在一起。
如果您還沒有的話,現在是觀看演講的好時機:
在我的演講中,我聲稱就像monads是純函數式編程的超級抽象一樣,延續是命令式編程的超級抽象,并且引入了一種抽象,我稱為“作用域延續”,這僅是帶分隔符的延續 。一種特殊的醬料(我不知道這個概念是否在其他地方討論過;如果有,我很想知道它的專有名稱(請參閱文章末尾的附加內容))。
由于我在演講之前不久就想到了這個主意,因此在介紹范圍內的延續時我并沒有做好準備,并且由于此后我最近對該主題進行了更多考慮,所以我想繼續討論這個主意。 我提出了三點主張:
我認為我為第1點說明了理由,因為范圍內的延續使您可以保持命令式控制流,并且它們保留了堆棧上下文,這對于事后調試和性能分析至關重要。 當談到#2時,我更加模糊,直覺地注意到了monad與續奏之間的聯系,并提供了一些示例,但沒有提供證明,因此聽眾理所當然地要求我這么做。
第一輪:連鎖–定界延續vs.單子
演講結束后,我與朱利安·阿尼 ( Julian Arni )進行了交談,后者向我展示了丹·皮波尼 ( Dan Piponi)撰寫的博客文章《所有人的母親》 。 有關Reddit的討論 )使我想到了由Andrzej Filinski 1于1994年提出的證明 ,定界的連續性(在Filinski的論文中稱為部分或可組合的連續性)可以表示任何一元組合。 他說:
我們表明,任何其單元和擴展操作可表示為純功能術語的monad都可以通過“可組合的延續”嵌入按值調用語言中…
…值得注意的是,monad對“不純”函數式編程沒有可比的影響。 可能的主要原因可能是……單子框架已經內置在渴望有效的功能性語言的語義核心中,因此無需明確表達。 語言(例如,可更新狀態,異常或一流的延續)和語言外部(I / O,OS接口等)的“不純凈”構造都遵循一元法。 似乎唯一缺少的方面是程序員能夠以與內置效果相同的便捷性和自然性使用自己的,特定于應用程序的單子抽象(例如,不確定性或解析器)。
……在下文中,我們將證明……具有第一類延續性的一種語言……已經“單調完成”,因為任何以某種扭曲的單調風格表達的程序也可以直接編寫。
我沒有遵循Filinski論文的必要背景,但是,如果我沒有記錯的話,證明的困難源于以下事實:從單子形式到連續形式(他稱之為“直接樣式”)的轉換是不是單調函數或單調作曲者的簡單數學映射(Haskell稱之為bind ),而是需要對其源代碼表示進行更深層次的轉換。 但是,我將以一種有希望的方式介紹定界連續的具體實現方式,以期解釋moand-continuation相似性背后的直覺。
定界的延續捕獲了調用堆棧的一部分。 它使我們暫停計算然后再恢復它。 讓我們看一下Java中定界的延續API:
public class Continuation<T> implements Runnable, Serializable, Cloneable {public Continuation(Callable<T> target) { ... }public T run() { ... }public boolean isDone() { ... }public T getResult() { ... }public static Continuation<?> suspend(Consumer<Continuation<?>> ccc) { ... } }suspend方法(類似于Scheme的shift )暫停當前的延續(假設我們正在內部運行),并調用(可選)提供的回調ccc (名稱ccc是Called with Current Continuation的首字母縮寫,這是一種玩法)在Scheme的call-cc )。 run功能(對應于Scheme的reset )將執行繼續,直到其暫停或終止。 因此,例如:
class Foo {static int foo() {bar();bar();return 3;}static void bar() {System.out.println("Pausing...");Continuation.suspend(null);}public static void main(String[] args) {Continuation<Integer> c = new Continuation(Foo::foo);c.run(); // prints "Pausing..."c.run(); // prints "Pausing..."c.run();System.out.println(c.getResult()); // prints "3"} }因為suspend返回了延續并將其傳遞給回調,所以我們可以擴展Continuation類并添加一些內部字段以產生ValuedContinuation :
public class ValuedContinuation<T, Out, In> extends Continuation<T> {private Out pauseOut;private In pauseIn;private RuntimeException pauseInException;public run(In in);public run(RuntimeException e);public Out getPauseValue() { ... }public static <Out, In> In pause(Out value) {...}public static <In> In pause(Consumer<ValuedContinuation<?, ?, In>> ccc) {...}public static <V, In> In pause(V x, BiConsumer<V, ValuedContinuation<?, ?, In>> ccc) {...} }ValuedContinutation ,我們可以將值傳入和傳出延續。 如果我們調用pause(3) ,則getPauseValue將返回值3 ,而如果使用run(5)恢復繼續,則將由pause返回值5 。 run(new RuntimeException())將導致pause以引發該異常。 例如:
ValuedContinuation<Void, Integer, Integer> c = new ValuedContinuation<>(() -> {int x = pause(5);x = pause(x + 10);x = pause(x * 100);return null;});while(!c.isDone()) {c.run(3);System.out.println(c.getPauseValue()); // prints: 5, 13, 300 }現在我們可以理解連續性可以表達任何monad的主張的直覺: 我們的monadic作曲者 (或bind ) 將是傳遞給pause的回調ccc ; 每次pause的代碼是c.run(x)序列中的下一個monadic函數,并且調用c.run(x)正在應用鏈中的下一個c.run(x)函數。
區別在于,單子函數將蹦床功能返回到封閉的作曲家(綁定),而在這里我們在延續中稱作作曲家(我們的ccc )。 正如我在演講中所聲稱的,命令式語言中的繼續性的優點是它們與所有命令式概念(例如命令式控制流和異常)良好地交互,并保留了對于調試和性能分析非常重要的堆棧上下文。
在繼續之前,讓我們看一個使用ccc回調的示例。 這是“未來單子”延續形式的一個例子。 假設我們有一個異步服務:
interface AsyncHandler<T> {void success(T result);void failure(RuntimeException error); }interface AsyncService<T> {void submit(AsyncHandler<T> callback); }然后,我們可以定義此方法:
static <T> Consumer<ValuedContinuation<?, ?, T>> await(AsyncService<T> service) {return c -> {service.submit(new AsyncHandler<T>() {public void success(T result) {c.run(result);}public void failure(RuntimeException error) {c.run(error);}});}; }我們將在延續中運行的代碼中使用該代碼,如下所示:
String y = pause(await(service));上面的代碼將暫停繼續,直到服務請求完成,然后將其恢復為結果。
第二輪:作曲–范圍延續與Monad變形金剛
在演講中,我還聲稱單子很難構成2 ,即使使用純功能語言也是如此,這非常適合單子。 編寫monad(即編寫使用異常和 IO 并產生序列的monadic代碼)需要使用monad變換器 ,因為它們利用非常高階的函數來形成一個讓人腦筋急轉的lambish間接函數 ,因此很難理解。
為了創建易于組合的延續,在我的演講中,我介紹了作用域延續 ,這是帶分隔符的延續的變體。 范圍內的延續是嵌套的延續,在任何級別,代碼都可以自由地暫停其任何包含的延續。 這個想法與嵌套的try / catch塊非常相似,在嵌套的try / catch塊中,根據異常類型,執行會跳轉到相應嵌套范圍的catch塊。
為了測試該想法在實踐中的效果如何,我已經在Java和Clojure中實現了一個有范圍的延續原型。 您可以分別在 Quasar和Pulsar的cont分支( 此處和此處)中使用作用域延續來查找代碼。
為了實現延續,我使用了Quasar的工具,該工具非常簡單(盡管有范圍的延續可能有一天會進入上游Quasar,但這種情況不會很快發生,因為我們首先需要使工具完全透明且可以不使用,我們希望Java 9發布時該怎么做)。 最困難的部分是支持在一個連續引用不僅存在于堆棧中,而且還可能存在于堆中的環境中,克隆嵌套的延續(下面介紹的非確定性延續所需要)。 我嘗試了三種不同的方法,但我對其中任何一種都不滿意。
對于范圍連續,我們需要稍微更改Continuation (和類似ValuedContinuation )類:
public class Continuation<S extends Suspend, T> implements Runnable, Serializable, Cloneable {public Continuation(Class<S> scope, Callable<T> target) { ... } // <-- scopepublic T run() { ... }public boolean isDone() { ... }public T getResult() { ... }public static Continuation<?> suspend(S scope, Consumer<Continuation<?>> ccc) { ... } // <-- scope }范圍是全局名稱。 在Java中,我選擇表示一個范圍,就像表示異常范圍一樣:作為類名(在當前實現中,范圍是擴展Suspend類,該類是異常類型)。
范圍的延續定義和使用方式如下:
class ACont<T> extends ValuedContinuation<AScope, T> {public Continuation(Callable<T> target) {super(AScope.class);// ...}public static AScope A = new AScope(); }// similarly BCont, and then:static void foo() {Continuation<Void> c = new ACont(() -> {// ...Continuation<Void> c = new BCont(() -> {// ...suspend(B, ...); // suspends the enclosing BCont// ...suspend(A, ...); // suspends the enclosing ACont// ...});// ...});// ... }在Clojure中,范圍是全局符號,并且可以將范圍的延續定義為:
(let ; ....(let ; ....(pause B ...); ...(pause A ...); ...))])))]; ... )范圍延續的概念是,暫停任何封閉的延續范圍相當于返回到任何封閉的作曲家(綁定)的單子函數。 但是在范圍連續的情況下,我們不需要monad變換器來轉換作曲者或鏈接的monadic函數。
為了了解這種組合在實際代碼中的外觀,我實現了兩種延續類型: CoIterable (與Python生成器一樣,生成具有延續的Iterable并對應于Haskell的list monad)和Ambiguity (實現了不確定性計算)回溯a-la Scheme的amb并對應于Haskell的amb monad。
孤立地, CoIterable的用法如下:
Iterable<Integer> range(int from, int to) {return new CoIterable<>(() -> {for (int i = from; i < to; i++)produce(i);}); }有關CoIterable運算符的示例,例如flatmap , map和filter請參見此處 ,并注意,額外的靈活性延續給了我們單子。 由于單子函數將蹦床返回給作曲者,因此必須根據單個平面映射作曲者來實現filter和map操作,而對于延續,我們可以從延續中自由選擇自己的構圖規則,并且可以實現filter并獨立于flatMap進行map ,以獲得更好的性能。
這是隔離中使用Ambiguity的示例:
Ambiguity<Integer> amb = solve(() -> {int a = amb(1, 2, 3); // a is either 1, 2, or 3int b = amb(2, 3, 4); // b is either 2, 3, or 4assertThat(b < a); // ... but we know that b < areturn b;});amb.run(); // returns 2 as that's the only possible solution for b現在,讓我們看看兩者是如何無縫組合的:
Ambiguity<Integer> amb = solve(() -> {Iterable<Integer> a = iterable(() -> {produce(amb(2, 1)); // pauses on Ambiguity and CoIterableproduce(amb(3, 10));});int sum = 0;for (int x : a) { // using imperative loops on purpose; functional would work, toosum += x;assertThat(x % 2 == 0); // we assert that all elements are even}return sum; });amb.run(); // returns 12注意如何a延續中止既對Ambiguity以及對CoIterable范圍。 它創建一個列表,第一個元素為2或1 ,第二個元素為3或10 ,產生四個可能的列表: (2, 3) , (2, 10) , (1, 3)和(1, 10) 。 后來,我們斷言所有元件必須是偶數,這意味著對于唯一有效的列表a是(2, 10)以及用于唯一可能的值sum是12。
作為最后一個示例(可以在此處和此處的測試中找到更多示例;可以在此處找到Clojure示例),讓我們通過另一層嵌套將事情進一步復雜化:
Fiber<Integer> f = new Fiber<>(() -> {Ambiguity<Integer> amb = solve(() -> {Iterable<Integer> a = iterable(() -> {produce(amb(2, 1));sleep(20); // pauses on the Fiber scopeproduce(amb(3, 10));});int sum = 0;for (int x : a) {sum += x;Fiber.sleep(20);assertThat(x % 2 == 0);}return sum;});return amb.run(); }).start();f.get(); // returns 12現在,我們將整個內容嵌套在光纖中-Quasar的輕量級線程實現-僅僅是Java的ForkJoin調度程序調度的延續而已。 現在,內嵌套代碼a在三個不同范圍內暫停沒有打破汗水,沒有任何形式的變壓器。
但是類型安全呢?
Haskell具有非常豐富的類型系統,而Monad可以極大地發揮作用。 通過查看(monadic)函數的簽名,您可以立即知道它可以“駐留”在哪種monad類型中,并且不能在該monad之外的任何地方使用它。 事實證明,可以在不失去其任何期望屬性的情況下,對作用域連續進行同樣類型的安全鍵入。 為此,我們需要一個(簡單的)類型系統來聲明:
void foo() suspends A, B這意味著foo可能會在A和B范圍內暫停繼續執行,因此只能在兩個范圍內的代碼中調用。 然后,將Continuation類定義為(在偽Java中):
public class Continuation<S extends Suspend, T> implements Runnable, Serializable, Cloneable {public Continuation(Class<S> scope, [Callable<T> suspends S|Others] target) { ... }public T run() suspends Others { ... }public static Continuation<?> suspend(S scope, Consumer<Continuation<?>> ccc) suspends S }因此,延續可以運行任何可能在參數化的S范圍以及其他范圍上可能掛起的目標代碼,而run方法可以吞咽S范圍,但仍在掛起其他范圍。
事實證明,我們已經有了這樣的類型系統- 幾乎是 Java的檢查異常。 如果我們創建了Suspend范圍(所有范圍都從該范圍下降),則我們可以使用Java的throws ,就像上面的偽Java中的suspend一樣。 我之所以沒有這樣做,是因為Java的類型系統不允許您捕獲多個經過檢查的異常類型,就像我在上述“ Others所做的那樣,這意味著我們需要顯式的實例來處理顯式的范圍變量(掛起一個范圍的函數,兩個范圍等),這可能會使事情變得麻煩。
然后,我們還可以通過參數化范圍來提高ValuedContinuation的類型安全性,這樣我們就可以:
void foo() suspends CoIterableScope<Integer>這只會讓foo在產生一個Integer序列(而不是String )的CoIterable中被調用。 不幸的是,我們也不能完全做到這一點,因為Java當前不允許泛型異常類型。
未完待續?
我希望通過更深入地討論范圍內的連續性,我能夠比我在演講中揮舞過的手揮舞的方法更好地解釋這個想法,并且我很高興找到菲林斯基的證明(這在PL圈子中可能是眾所周知的) )。
我希望我的演講使您相信單語在命令式語言中沒有地位(也許除了并行計算之外),如果沒有,我很想聽聽為什么不這樣做。 我還相信,即使在PFP語言中,范圍連續的合成也比monad更好(而且,monad通常不是一種很好的效果建模方法,但這是另外一個討論)。
最后,盡管我堅信命令性語言應該具有某種形式的輕量級線程(AKA光纖,AKA用戶模式線程,AKA綠線程排序)和線程(任何類型)不過是由適當的調度程序調度的延續,我不一定認為命令性語言應該直接將范圍化的延續作為抽象公開。 畢竟,存在抽象以增加代碼重用性,幫助代碼維護和幫助驗證:總之,它們存在是為了降低開發成本,并且(至少從非研究的角度來看)它們是唯一的度量標準判斷3 。 我認為延續性是PFP優雅的monad的優雅命令,但是我還不相信它們在實踐中的實用性。
如果您想了解更多有關延續的知識,這就是延續發展的歷史,它可以稱贊所有合適的人。
附錄1
自從首次發布此博客文章以來,我設法在Philip Wadler于1993年發表的一篇論文中找到了關于范圍延續的參考,該論文名為Monads and composablecontinuations ,他將范圍延續簡單地稱為“具有多個層次的可組合延續”。 沃德勒證明定界的延續可由單子表示,而菲林斯基證明(一年后),二元論可表示為定界的延續,這有理由推論兩者是對偶的。 盡管如此,有理由認為,即使是對偶,每種也都更適合于特定的編程風格,并且毫無疑問,延續更適合于不純潔的按值調用的語言(命令式和函數式命令式)。 瓦德勒在總結論文時說:
具有多個層次的可組合延續的一個目標是能夠將不同的影響分解為不同的層次。 Danvy和Filinski聲稱以這種方式將各種效果均勻地組合起來相對容易。 Monads還旨在通過簡化組合的方式來分解效果。 但是,沒有統一的規則來組合任何兩個單子。 本文使用了monad來闡明可組合的延續。 可組合的延續詞會闡明單子組合的問題嗎?
附錄2
在網上討論中,一位讀者評論說,我通過談論單子而不是單子來誤解了單子。 我認為這僅是解釋上的差異,因此我想澄清一下:
正如已經證明(我認為)的那樣,任何效果都可以由單子模擬,您可以說所有效果都是單子的,但是就像著名笑話中的數學家一樣,這是絕對正確的,但絕對沒有用(取決于您的觀點)。 -視圖,我猜)。
從數學的角度看,只要兩件事同構,它們就是“相同”的。 但是從編程的角度來看,兩者可能是非常不同的,因為抽象是與程序員思想上的心理交互,而兩個同構的數學概念在心理上與程序員之間的交互也非常不同。 因此,如果在處理抽象時我不必“思考單子”,那么即使它們之間存在同構,抽象也不是單子。
根據數學解釋,“反對單子”與反對數字1一樣荒謬。在我的解釋中,用阿拉伯數字,教堂數字或集合論數字表示數字1在心理上有很大不同,并且因此,在編程語言上有根本不同,因為編程語言首先是人類語言。 在一種編程語言中,抽象是通過數學以及心理(或經濟)特性來定義(和測量)的。
我是一個“算法論者”,而不是一個“抽象論者”(不幸的是,我認為這兩個CS觀點常常是矛盾的),因此我僅在抽象化在編寫和維護方面帶來的成本變化方面衡量其有用性我的算法,對我來說,單子是一種設計模式,而不是以某種特定符號表示的數學對象。
翻譯自: https://www.javacodegeeks.com/2015/08/from-imperative-to-pure-functional-and-back-again-monads-vs-scoped-continuations.html
總結
以上是生活随笔為你收集整理的从命令式功能到纯功能性,然后再返回:Monads与范围内的延续的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: TGA是什么文件 TGA文件是什么意思
- 下一篇: 安顺住建局备案查询网(安顺住建局备案查询