javascript
JavaScript, ABAP和Scala里的尾递归(Tail Recursion)
這是Jerry 2021年的第 12 篇文章,也是汪子熙公眾號總共第 283 篇原創文章。
今天是2021年1月20日,看看歷史上的今天都發生了什么。
2004年1月20日,第一個公開版本的Scala發布。
Scala是一種采用靜態類型系統的編譯型語言,具有很強的可擴展性(Scalability),這也是其名稱的由來。
Scala設計初衷是集成面向對象編程和函數式編程的各種特性,運行于JVM平臺上,并兼容已有的Java程序。
Jerry沒有在SAP標準產品開發中使用過Scala,只是完成2015年公司一個內部培訓布置的課程作業中,使用Scala在Spark上開發了一個最簡單的demo:統計海量英文圖書里,計算出使用頻率最高的十大單詞。
Spark是一個使用Scala編程語言實現的專為大規模數據處理而設計的快速通用的計算引擎。本文不會討論Spark,而是從Scala語言里,下圖第11行的注解@tailrec談起:尾遞歸(Tail Recursion).
每個程序員對遞歸的概念都耳熟能詳,那什么是尾遞歸呢?
顧名思義,如果一個函數中遞歸形式的調用,出現在函數的末尾,且除了該遞歸調用外,不包含其他的運算操作,則我們稱該遞歸函數是尾遞歸函數。
本文用階乘算法來介紹尾遞歸的概念。
下圖紅色區域內是階乘算法的常規遞歸實現,藍色區域是階乘算法的尾遞歸實現版本。在常規遞歸算法的末尾,第8行語句(綠色),除了遞歸調用factorial函數外,還包含一個同n的乘法操作,所以整個函數factorial不能算作尾遞歸函數。
而尾遞歸版本中,第14行函數末尾(黃色),僅僅包含函數本身的遞歸調用,所以整個函數tailFactorial是一個尾遞歸函數。
尾遞歸函數存在的意義是什么呢?要回答這個問題,我們可以先在單步調試模式下,觀察常規遞歸函數的執行過程。
我們首先使用常規遞歸函數,計算5的階乘。
輸入參數n為5,執行到第7行,5的階乘等于5乘以4的階乘。單步調試進去,輸入參數n = 5, 進入第7行,準備執行 5 * factorial(4) .
注意觀察下圖的Call Stack列表,此時我們已經有兩個factorial函數的調用棧幀了。
什么是棧幀?復習一下大學計算機原理學到的知識:在函數執行過程中,每一個函數調用都會把當前函數的調用信息和內部變量保存在棧里面,稱為一個棧幀(Stack Frame).
其中下圖序號為1的棧幀,保存了n = 5的計算上下文;序號為2的棧幀即當前最頂層的棧幀,保存了n = 4的計算上下文。
因為只有當n = 1時遞歸才會結束,而當前n = 4,所以繼續單步調試第7行:又生成了一個n = 3的棧幀:
n = 2:
終于我們來到了n = 1的上下文??聪聢DCall Stack里的棧幀列表,最頂層的棧幀代表當前n = 1的計算上下文。此時我們已經知道n = 1的階乘結果如何計算了,即為1本身。
第5行代碼返回1的階乘計算結果1,這行語句返回之后,當前序號為5的棧幀就會被銷毀,即將回到下一層序號為4的棧幀去。
此時只剩4個棧幀了,最頂層代表n = 2的棧幀。因為現在1的階乘結果已經出來了,所以2的階乘結果也能計算了,為2乘以1.
2的階乘返回后,現在只剩3個棧幀,最頂層為n = 3的計算上下文。3的階乘也能計算了,為3乘以前一個棧幀返回的計算結果,即2的階乘結果,所以最后為3 × 2 = 6. 如下圖所示:
4的階乘計算,此時只剩兩個棧幀:
5的計算結果,回到最初最先被壓到堆棧底部的n = 5的棧幀。計算完畢,5的階乘為120.
是不是體現出了《數據結構》教科書上關于?!跋冗M后出”的工作原理?
下面再來看看用尾遞歸實現的階乘。
下圖第20行語句是以尾遞歸方式計算5的階乘入口,調用尾遞歸函數tailFactorial,注意函數的第二個輸入參數total,這個參數用于存儲當前階乘的計算結果。
這個尾遞歸函數的結束條件是,當第一個輸入參數n為1時,就把第二個輸入參數的值,作為階乘運算的最終結果返回。第二個參數實際上存放的,是當前遞歸調用的階乘計算結果。
當n大于1時,遞歸尚未滿足退出條件,此時首先將n和當前的階乘計算結果(變量total)相乘,將乘積作為第二個輸入參數,傳遞到下一層遞歸調用的棧幀中去。
下圖是tailFactorial函數內部,即將進入第一輪遞歸調用的棧幀:
第一輪遞歸調用的棧幀內部,序號為2.
注意,此時序號為1的棧幀已經完全不再需要了,因為我們繼續進行遞歸調用的所需信息,都已經包含在第16行tailFactorial調用的兩個輸入參數里了,此時n為上一層遞歸調用傳入的5 - 1 = 4,total為上一輪傳入的5 × 1 = 5. 進行下一輪遞歸調用,兩個輸入參數的值分別是4 - 1 = 3和4 * 5 = 20.
進入第三層遞歸調用,此時輸入參數 n = 3,total = 20,均為上一層調用傳入。
注意,下圖標號為1和2的兩個棧幀,實際上不再需要了,因為要繼續進行遞歸調用的所有輸入信息,都已經存儲在標號為3的棧幀里了:
n = 2, total = 60,同理,標號為1,2,3的棧幀都不再需要了。
n = 1,total = 120,終于計算結束了!這就是5的階乘,如何通過尾遞歸的方式計算出來的全過程。
我們在標號為5的棧幀里得到了最終的結果,而此時雖然棧幀1~4還存在,但實際上已經毫無用處了。
因為按照尾遞歸版本的階乘實現,每一輪階乘的遞歸計算結果,已經通過第二個參數total保存了下來,因此沒有必要再用一個完整的棧幀,去保存當前這輪遞歸計算的函數調用上下文了。這就引出了所謂“尾遞歸優化”的概念:
When a compiler detects a call that is tail recursive, it overwrites the current activation record instead of pushing a new one onto the stack. The compiler can do this because the recursive call is the last statement to be executed in the current activation; thus, there is nothing left to do in the activation when the call returns. Consequently, there is no reason to keep the current activation around. By replacing the current activation record instead of stacking another one on top of it, stack usage is greatly reduced, which leads to better performance in practice. Thus, we should make recursive functions tail recursive whenever we can.
https://www.oreilly.com/library/view/mastering-algorithms-with/1565924533/ch03s02.html
上述文字大意如下:
當(C語言)編譯器檢測到尾遞歸調用時,并不會創建新的棧幀并壓入棧中,而是用新的棧幀覆蓋掉當前處于激活狀態的棧幀。編譯器之所以能夠這樣做,是因為尾遞歸函數里,遞歸調用是當前棧幀里最后一個需要執行的函數調用。被覆蓋掉的棧幀本身毫無用處,不需要再保留。采用棧幀覆蓋,而不是新建棧幀的方式,極大程度上減少了棧幀的個數,提高了遞歸函數的執行性能。因此,應該盡可能地去嘗試使用尾遞歸方式實現遞歸函數。
一個實際的性能比較例子:計算20的階乘,二者的性能有巨大差異:普通遞歸實現需要10毫秒,而尾遞歸實現僅僅需要不到1毫秒的時間。
注意:一個遞歸函數能否用尾遞歸方式實現,和它能否享受運行時的尾遞歸優化,二者不是一回事,后者需要編譯器的支持。
應用開發人員通過Scala提供的@tailrec注解,告訴編譯器,對注解修飾的方法進行尾遞歸優化:
如果優化失敗,或者被修飾的方法根本就不是一個尾遞歸函數,則編譯器報錯:
could not optimize @tailrec annotated method fibonacci: it contains a recursive call not in tail position
用ABAP實現尾遞歸版本的階乘運算:
至于ABAP編譯器能否支持尾遞歸優化?我沒有研究過,我只是覺得,尾遞歸優化并不能算是ABAP編譯器必須實現的需求之一。
希望本文能幫助大家對尾遞歸優化這個概念有一個最基本的認識,感謝閱讀。
ABAP專題
-
Jerry的ABAP, Java和JavaScript亂燉
-
ABAP開發人員未來應該學些什么
-
Jerry 2017年的五一小長假:8種經典排序算法的ABAP實現
-
Jerry的ABAP原創技術文章合集
-
300行ABAP代碼實現一個最簡單的區塊鏈原型
-
使用Java+SAP云平臺+SAP Cloud Connector調用ABAP On-Premise系統里的函數
-
在SAP云平臺的CloudFoundry環境下消費ABAP On-Premise OData服務
-
ABAP vs Java, 蛙泳 vs 自由泳
-
聊聊C語言和ABAP
-
動手使用ABAP Channel開發一些小工具,提升日常工作效率
-
我用ABAP做過的那些無聊的事情
-
不喜歡SAP GUI?那試試用Eclipse進行ABAP開發吧
-
使用Visual Studio Code編寫和激活ABAP代碼
-
你的ABAP程序給佛祖開過光么?來試試Jerry這個小技巧
-
在SAP云平臺ABAP編程環境上編寫第一段ABAP程序
-
SAP官方發布的ABAP編程規范
-
ABAP Code Inspector那些隱藏的功能,您都知道嗎?
-
還在用ABAP進行SAP產品的二次開發?來了解下這種全新的二次開發理念吧
-
ABAP Netweaver體內的那些寄生式編程語言
-
從SAP社區上的一篇博客開始,聊聊SAP產品命名背后的那份情懷
-
云端的ABAP Restful服務開發
-
如何在SAP云平臺ABAP編程環境里把CDS view暴露成OData服務
-
使用abapGit在ABAP On-Premises系統和SAP云平臺ABAP環境之間進行代碼傳輸
-
30分鐘用Restful ABAP Programming模型開發一個支持增刪改查的Fiori應用
-
Jerry帶您了解Restful ABAP Programming模型系列之二:Action和Validation的實現
-
Jerry帶您了解Restful ABAP Programming模型系列之三:云端ABAP應用調試
-
SAP云平臺上的ABAP編程環境里如何消費第三方服務
-
ABAP開發者上云的時候到了 - 現在大家可以免費使用SAP云平臺ABAP環境的試用版了
-
學而不思則罔 - SAP云平臺ABAP編程環境的由來和適用場景
-
SAP云平臺里的三叉戟應用
-
如何基于Restful ABAP Programming模型開發并部署一個支持增刪改查的Fiori應用
-
SAP 2019 TechEd Key Note解讀:云時代下SAP從業人員如何做二次開發?
-
有哪些ABAP關鍵字和語法,到了ABAP云環境上就沒辦法用了?
-
ABAP開發環境終于支持以駝峰命名法自動格式化ABAP變量名了
-
利用ABAP 740的新關鍵字REDUCE完成一個實際工作任務
-
一段讓人瑟瑟發抖的ABAP代碼
-
昨日萬圣節ABAP怪獸級代碼謎團,公布答案啦
-
介紹一種在ABAP內核態進行內表高效拷貝的方法
-
使用SAP Cloud Application Programming模型開發OData的一個實際例子
-
當ABAP遇見普羅米修斯
-
使用ABAP繪制可伸縮矢量圖
-
ABAP開發環境語法高亮的那些事兒
-
SAP錯誤消息調試之七種武器:讓所有的錯誤消息都能被定位
-
使用ABAP操作Excel的幾種方法
-
SAP GUI里的收藏夾事務碼管理工具
-
SAP GUI和Windows注冊表
-
有了Debug權限就能干壞事?小心了,你的一舉一動盡在系統監控中
-
ABAP CCDEF, CCIMP, CCMAC, CCAU, CMXXX這些東東是什么鬼
-
實現ABAP條件斷點的三種方式
-
使用SAT跟蹤監控從瀏覽器打開的SAP應用的性能和調用棧
-
一個13年ABAP老兵的建議:了解這些基礎知識,對ABAP開發有百利而無一害
-
SAP ABAP Netweaver容器化, 不可能完成的任務嗎?
-
SAP產品增強技術回顧
-
SAP API開發方法大全
-
淺談Java和SAP ABAP的靜態代理和動態代理,以及ABAP面向切面編程的嘗試
-
SAP ABAP應用服務器的HTTP響應狀態碼(Status Code)
-
SAP ABAP里存在Java List這種集合工具類么?CL_OBJECT_COLLECTION了解一下
-
ABAP面試題系列:寫一組會出現死鎖(Deadlock)的ABAP程序
-
SAP ABAP Netweaver服務器的標準登錄方式講解
-
SAP ABAP關鍵字語法圖和ABAP代碼自動生成工具Code Composer
-
SAP ABAP SM50的另類用途 - ABAP工作進程對數據庫表讀取操作的檢測
-
關于SAP ABAP字符變量和字符串變量字符個數的一個知識點,和一個血案
-
SAP ABAP一組關鍵字 IS BOUND, IS NOT INITIAL和IS ASSIGNED的用法辨析
-
SAP ABAP和Java里的弱引用(WeakReference)和軟引用(SoftReference)
-
SAP AMDP介紹 - ABAP托管的HANA數據庫過程
-
給你的ABAP對象打上標簽(Tag)
-
歷史上的今天:編程語言中null引用的十億美元錯誤
更多Jerry的原創文章,盡在:“汪子熙”:
總結
以上是生活随笔為你收集整理的JavaScript, ABAP和Scala里的尾递归(Tail Recursion)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【初码干货】使用阿里云邮件推送服务架设自
- 下一篇: 如何让SAP Spartacus ng