成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

類型的本質和函數式實現

開發 前端
那么下面我們就先來認識一下類型到底是什么?我們先以來看看 表示元素對的Pair類型,可能有人一提到Pair類型馬上就會在腦海中浮現出下面的結構:

在上一篇文章《二叉樹迭代器算法》中,我介紹了一種基于棧的二叉樹迭代器實現。程序設計語言和Haskell大牛@九瓜 在看過之后評論到:

這里用了 stack 來做,有點偷懶,所以錯失了一個抽象思考機會。如果我們能夠理解二叉樹到線性表的轉換過程,完全可以把 Iterator 當作抽象的線性表來看,只要定義了關于 Iterator 的 empty, singleton, 還有 append 操作,實現二叉樹的 Iterator 就變得非常直觀。

“錯失了一個抽象思考機會”是什么意思呢?我理解九瓜的意思是基于棧的實現雖然是正確的,但它缺乏對于迭代器類型本質的理解,不具有通用性。如果能 對迭代器進行合適地抽象就可以像二叉樹遞歸遍歷一樣自然地得出二叉樹迭代器,甚至其他更復雜的數據結構,只要我們能寫出它的遍歷算法,迭代器算法都可以自 然推出。

類型的本質

九瓜提到了通過empty, singleton和append操作對Iterator進行抽象,我本來打算直接根據這個思路介紹函數式的二叉樹迭代器實現,但是考慮到其實首要的問題 在于理解類型的本質,而并不是所有人都具備這個基礎,不如先普及一下類型基礎再進入具體實現。那么下面我們就先來認識一下類型到底是什么?我們先以來看看 表示元素對的Pair類型,可能有人一提到Pair類型馬上就會在腦海中浮現出下面的結構:

  1. struct Pair { 
  2.     int left; 
  3.     int right; 

其實,這種理解是非本質的,Pair完全可以用2個元素的數組來表示,第一個元素表示left,第二個元素表示right:

  1. struct Pair { 
  2.     int elements[2]; 

上面的兩種不同表示是類型的不同實現,而類型的本質是由操作(Operation)和操作間的關系或不變式(Invariant)所定義的,我們稱之為類型規范(Type Specification)。比如,Pair類型是這樣定義的:

  1. Type Pair: 
  2.     Operations: 
  3.         Pair make_pair(int x, int y) 
  4.         int get_left(Pair pair) 
  5.         int get_right(Pair pair) 
  6.     Invariants: 
  7.         get_left(make_pair(x, y)) == x  //對x, y構造的Pair取左元素等于x 
  8.         get_right(make_pair(x, y)) == y  //對x, y構造的Pair取右元素等于y 

也就是說只要是滿足Pair類型規范,即定義了make_pair,get_left, get_right這3種操作,并且這些操作滿足上面兩個不變式,那么它這就是Pair類型。我們再來看看稍微復雜一點的Stack類型:

  1. Type Stack: 
  2.     Operations: 
  3.         Stack make_stack()  //構造空棧 
  4.         Stack push(Stack stack, int x)  //將元素x壓棧,返回新棧 
  5.         int top(stack)  //獲取棧頂元素 
  6.         Stack pop(Stack stack)  //將棧頂元素彈棧,返回新棧 
  7.     Invariants: 
  8.         top(push(stack, x)) == x  //棧頂元素為最后一次壓棧值 
  9.         pop(push(stack, x)) == stack  //對stack壓棧后再彈棧等于原來的stack 

Stack類型規范簡言之就是FILO(先入后出),如果要形式化就是上面的不變式。為了加深理解,我們現在切換到測試視角來看一看,如果請你來編 寫一個Stack類的單元測試用例,你應該怎么寫呢?許多朋友都不清楚單元測試到底測什么?怎么測?我見過不少人用一個測試用例單獨對push做測試,用 另一個測試用例對pop單獨做測試,其主要原因就在于缺乏對類型本質的理解。其實,只要理解了類型的本質我們就知道孤立地看push或pop是沒有任何意 義的,它們的意義是在FILO關系下相互解釋的,所以測試當然是基于類型規范來測試FILO不變式!這種基于類型規范的測試是一種黑盒測試,與類型的內部 實現細節無關,只要單元測試覆蓋了類型所定義的所有操作和不變式,那么不管內部怎么實現或優化,測試用例都不需要調整。反之,如果深入到了類型的內部實現 做白盒測試,那這樣的測試用例實際上就不再是反映其類型規范了,它會隨著實現細節的調整而失效。

更深一層來看,不僅是在Pair,Stack這樣的微觀層面,在一個系統的宏觀層面同樣可以采用類型視角,即考察系統定義了哪些操作?這些操作之間 有什么樣的關系或不變式?比如,你如何從類型的角度來看待MySQL這樣一套數據庫系統?MySQL系統定義了哪些操作?這些操作之間必須滿足怎樣的關系 和不變式?不僅如此,類型視角除了可以應用于計算機系統,甚至還可以應用于生活中的事物,比如,你到超市購物可以寄存自己的包,在寄包的時候會獲得一張密 碼條,取包時可以通過密碼條打開箱子。你能從超市寄包這個例子中看出類型來嗎?如果你看出來了,說明你對類型的理解真正融會貫通了!

類型的函數式實現

上面我們介紹了類型的本質在于操作和操作間的關系,下面我們要關注的是類型的實現。在上面的例子中,Pair的內部結構到底是什么,是一個left 和一個right成員?還是一個兩元素的數組?沒有講,也沒關系,就好像Windows的Handle和Linux的FileDescriptor一樣, 它們都是一個標識,你并不需要關心它的值本身,你只需要用幾個相關的函數創建和操作它就行了(上面超市寄包例子中的密碼條和Windows中的 Handle是什么關系,你看出來了嗎?你需要理解密碼條的內容嗎?)。換句話說,只要滿足類型規范,具體實現是可變的,使用者只依賴于類型規范而不依賴于其具體實現。這在面向對象語言中意味著接口保持不變而具體實現可以變化(這里把public方法視為一種廣義的接口)。

下面,我們還會看到的是不僅類型的內部實現可以變化,而且可以根本沒有什么所謂的內部實現。這是什么意思呢?讓我們來思考一下,是不是Pair內部一定要有什么東西來保存構造函數傳入的left和right?我們能跳出這個定勢嗎?在函數式編程中,我們能做到:

  1. //Javascript 
  2. function make_pair(x, y) { 
  3.     // 返回一個支持get_left和get_right操作的閉包(Closure) 
  4.     return { 
  5.         get_left : function() { return x }, 
  6.         get_right : function() { return y } 
  7.     } 
  8. function get_left(pair) { 
  9.     return pair.get_left(); 
  10. function get_right(pair) { 
  11.     return pair.get_right(); 
  12. // Test case 
  13. console.log(get_left(make_pair(1, 2))) //1 
  14. console.log(get_right(make_pair(1, 2))) //2 

#p#

上面的關鍵代碼在于make_pair的內部返回的不是一種具體的數據結構,而是一個支持get_left和get_right操作的閉包 (Closure),將來可以通過get_left和get_right來提取x, y。這種基于閉包的實現和我們通常采用的基于數據結構的實現的本質區別在哪里呢?不難發現,基于閉包的實現和類型規范是完全對應的, 它并沒有引入類型規范之外的東西,而基于數據結構的實現則隱藏了實現的細節。換句話說,如果要驗證實現代碼的正確性,對于前者只需要比對著類型規范,對于 后者我們可能需要去仔細理解推敲其所采用的數據結構。對于Pair這樣簡單的結構二者差別不大,甚至基于數據結構的實現更簡單,但是對于復雜的類型就容易 體現出閉包實現的優勢了。為了加深理解,我們再來看一個Stack的函數式實現:

  1. //Javascript 
  2. function make_stack() { 
  3.     return null 
  4. function push(stack, x) { 
  5.     return { 
  6.         top : function() { return x }, 
  7.         pop : function() { return stack } 
  8.     } 
  9. function top(stack) { 
  10.     return stack.top() 
  11. function pop(stack) { 
  12.     return stack.pop() 
  13. // Test case 
  14. var stack = make_stack() 
  15. stack = push(stack, 1) 
  16. stack = push(stack, 2) 
  17. stack = push(stack, 3) 
  18. console.log(top(stack)) //3 
  19. stack = pop(stack) 
  20. console.log(top(stack)) //2 
  21. stack = push(stack, 4) 
  22. console.log(top(stack)) //4 

上面的所有函數都是采用了無副作用的純函數式設計,可能習慣面向對象編程的朋友不是很習慣,不過這不影響我們對類型的討論,而且它也很容易改造成面向對象的風格,感興趣的朋友可以自己嘗試對上面的代碼進行簡單的包裝讓它看起來像面向對象的樣子。

函數式二叉樹迭代器

上面我們介紹了類型的本質和函數式實現,下面我們再來看看Iterator類型又該如何定義和實現呢? 思路當然還是從操作入手,考慮Iterator類型對應了哪些操作,它們的關系是什么?上面九瓜提示了Iterator類型可以抽象為線性表List類 型,或者說Iterator本質上是一個List。為什么呢?其實,只要跳出“如何表示數據結構”的思維,從類型角度思考就很容易理解,因為 Iterator和List都定義了相同的操作,Iterator的使用者完全不關心也不知道它背后到底是鏈表還是二叉樹,你對Iterator的操作和 一個List的操作完全一樣。正是這個原因,STL等范型庫才能通過Iterator將算法和數據結構解耦。

怎么定義一個List類型呢?九瓜提到的empty(), singleton()和append()實際上就是和List打交道最多的Lisp語言的經典定義方式。Lisp是基于s-expression 的,s-expression既可以視為線性表又可以視為樹,本質上Lisp為List類型了構造、取首元素和取剩余元素等幾種操作:

  1. Type List: 
  2.     Operations: 
  3.         List empty()  //構造空表,通常由()這個文字量表示 
  4.         List singleton(Element e)  //構造包含一個元素的表,通常由(e)這個文字量表示 
  5.         Element first(List list)   //取list的第一個元素,通常又叫car操作 
  6.         List rest(List list)  //取list除第一個元素外的剩余部分,通常又叫cdr操作 
  7.         List append(List list1, List list2) //連接兩個表 
  8.     Invariants: 
  9.         append(empty(), list) == list  //空表和表list連接后等于表list 
  10.         append(list, empty()) == list  //空表和表list連接后等于表list 
  11.         first(singleton(e)) == e  //對singleton(e)取首元素等于e 
  12.         rest(singleton(e)) == empty()  //對singleton(e)取首元素外的剩余部分的結果為空表 
  13.         append(first(list), rest(list)) == list  //對list的首尾兩部分進行連接等于list本身 
  14.         if list1 is not empty then 
  15.             first(append(list1, list2)) == first(list1)  //對非空表list1于表list2的連接取首元素等于對非空表list1取首元素 
  16.         if list1 is not empty then 
  17.             rest(append(list1, list2)) == append(rest(list1), list2)  //對非空表list1于表list2的連接取首元素等于對非空表list1取首元素 

有了上面的分析,我們相應地寫出下面的List實現:

  1. //Javascript 
  2. function empty() { 
  3.     return null 
  4. function singleton(e) { 
  5.     return { 
  6.         first: function() { return e }, 
  7.         rest: function() { return null } 
  8.     } 
  9. function first(list) { 
  10.     return list.first() 
  11. function rest(list) { 
  12.     return list.rest() 
  13. function append(list1, list2) { 
  14.     if (null == list1) return list2 
  15.     if (null == list2) return list1 
  16.   
  17.     return { 
  18.         first : function() { return first(list1) }, 
  19.         rest : function() { return append(rest(list1), list2) } 
  20.     } 

#p#

在此基礎上可以進一步實現二叉樹迭代器:

  1. function make_binary_tree_iterator(node) { 
  2.     return { 
  3.         first : function() { 
  4.             return null != node.left ? first(make_binary_tree_iterator(node.left)) : node 
  5.         }, 
  6.         rest : function() { 
  7.             var left_it = (null == node.left ? null : make_binary_tree_iterator(node.left)) 
  8.             var root_it = singleton(node) 
  9.             var right_it = (null == node.right ? null : make_binary_tree_iterator(node.right)) 
  10.             var it = append(append(left_it, root_it), right_it) 
  11.             return rest(it) 
  12.         } 
  13.     } 
  14. //======== Test case ======== 
  15. var tree = { 
  16.     value : 1, 
  17.         left : { 
  18.             value : 2, 
  19.             left : { value : 4, left : null, right : null }, 
  20.             right : null 
  21.         }, 
  22.         right : { 
  23.             value : 3, 
  24.             left : null
  25.             right : { value : 7, left : null, right : null } 
  26.     } 
  27. for (var it = make_binary_tree_iterator(tree); null != it; it = rest(it)) { 
  28.     console.log(first(it).value) 

上面的make_binary_tree_iterator在List類型的基礎上按照二叉樹遍歷過程構造了一個List。不知道你是否注意到了,為什么它不像下面這個例子一樣直接返回一個List,而要構造一個閉包呢?

  1. function make_binary_tree_iterator(node) { 
  2.     var left_it = (null == node.left ? null : make_binary_tree_iterator(node.left)) 
  3.     var root_it = singleton(node) 
  4.     var right_it = (null == node.right ? null : make_binary_tree_iterator(node.right)) 
  5.     return append(append(left_it, root_it), right_it) 

這里關鍵的區別在于閉包是惰性求值的,也就是說只有當真正開始迭代遍歷的時候才會逐漸對各個函數進行求值,而上面的函數遞歸調用是非惰性的,會從一開始就把所有結點展開成線性表。如果你對這一點還不能很好地理解,可以嘗試在各個函數中加log跟蹤函數的調用過程。

總結

本文介紹了類型的本質在于它所定義的操作以及操作之間的關系和不變式。類型的實現關鍵在于滿足類型規范的要求,而具體實現是可以變化的,使用者和測 試用例都應該只依賴于類型規范而不依賴于具體實現。函數式的類型實現往往和類型規范是直接對應的,簡單通用,但可能有性能問題,而命令式的類型實現往往會 引入復雜的內部數據結構,但是其優點是高效。這兩種實現并不是完全互斥的,有時候可以將二者相結合達到簡單與高效的結合。

原文鏈接:http://coolshell.cn/articles/10169.html

責任編輯:陳四芳 來源: 酷殼網
相關推薦

2021-08-18 07:56:05

Typescript類型本質

2025-05-20 08:10:00

函數函數類型函數指針類型

2023-05-06 07:27:47

2021-03-27 10:54:34

Python函數代碼

2012-12-13 10:58:41

IBMdW

2020-10-09 08:26:16

架構

2009-08-27 16:22:58

interfaceabstract cl

2018-04-04 14:29:33

2010-03-11 11:10:14

Python函數式

2020-05-26 21:17:28

函數式編程純函數

2020-05-26 16:27:58

函數孩子編程

2020-09-23 16:07:52

JavaScript函數柯里化

2023-03-20 08:14:11

PHP類型轉換

2012-03-21 09:30:11

ibmdw

2011-10-19 15:47:13

2009-11-23 09:34:05

WPF本質

2021-08-12 10:38:58

安全分析數據安全網絡安全

2023-11-21 07:17:36

Reac函數組件

2020-02-06 19:12:36

Java函數式編程編程語言

2009-09-27 15:29:00

Scala講座面向對象Scala
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 99国产精品久久久久久久 | 成年网站在线观看 | 国产一区二区精品自拍 | 黄色一级免费 | 99亚洲精品| 国产精品性做久久久久久 | 色婷婷av一区二区三区软件 | 国产日韩欧美一区二区在线播放 | 亚洲国产精品一区 | 免费观看一级特黄欧美大片 | 一区二区三区四区免费在线观看 | 91青娱乐在线 | 丁香久久| 亚洲精品一区在线 | 国产精品美女一区二区三区 | 夜夜骑首页 | 精品久久国产 | 嫩草一区二区三区 | 久久69精品久久久久久国产越南 | 精品亚洲一区二区三区 | 国产精品久久久久久 | .国产精品成人自产拍在线观看6 | 欧洲一区二区在线 | 黄色操视频| 日韩在线视频网址 | 91亚洲精| 久久精品成人 | 精品乱人伦一区二区三区 | 成人免费视频观看 | 国产日韩一区二区三区 | 亚洲国产精品99久久久久久久久 | 日韩欧美国产一区二区 | 黑人精品 | 日本精品久久 | 久久久久国产精品免费免费搜索 | 欧美黄色大片在线观看 | 中文字幕精| 日韩视频在线免费观看 | 中国黄色毛片视频 | 午夜欧美| 国产我和子的乱视频网站 |