閉包還能夠這樣寫?談談少兒編程工具的實現思路

  版權申明:本文爲博主窗戶(Colin Cai)原創,歡迎轉帖。如要轉貼,必須註明原文網址

  http://www.cnblogs.com/Colin-Cai/p/11601046.html 

  做者:窗戶

  QQ/微信:6679072

  E-mail:6679072@qq.com

  詭異的代碼  html

 

  

  看看這段代碼,很明顯,是列舉出100之內全部的質數。相似這樣的程序咱們從學程序開始寫過不少。前端

  再仔細看看,這種「語言」彷佛有點像咱們學過的其餘語言,但彷佛並沒見過,語法有那麼一點點古怪?!linux

  

 

  哦!看到了,原來是一段Python!程序員

 

  

  上面代碼的執行結果是算法

  2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97sql

 

  再想一想,這段程序自己就是一堆函數組成,全都是Python的函數,並且全都做爲run函數的參數,甚至參數的參數這樣調用執行。好詭異的代碼!若是想一想這個如何實現的,怕是有點費腦子吧。編程

  

  面向過程的積木編程json

  

  目前少兒編程很流行,世面上有不少平臺。最有名的要數Scratch,其餘不少的少兒教育平臺大都是模仿Scratch的思路。數組

  

  上面是生成100之內質數的Scratch程序,在代碼堆裏打滾的咱們,即便從沒有見過Scratch,很快也會發現上述這樣的積木和咱們的C語言、Python等常見的面向過程的程序看起來區別不是很大。因而,咱們很快就可使用Scratch進行編程。微信

  Scrach抽象出了變量、各類算術運算,抽象除了過程式(命令式)編程所用到的順序、循環、條件分支;甚至於Scratch還能夠造積木,這必定程度上模擬了函數的意義,並且積木還能夠遞歸;但Scratch下沒有break、continue,這在必定程度下使得程序更難編了一些。另外,Scratch甚至模擬了消息(或者說信號)觸發這樣的抽象,目的多是爲了使編程更加有趣味性。

  

  理論上,積木編程固然也能夠支持break、continue等,甚至能夠goto。

 

  最普通的實現 

 

  本文不打算花太多篇幅來進行前端設計的描述,儘管是一個很複雜的話題。不過設計思路能夠簡單的說一下。

  首先,咱們爲每一種積木建模,不管是Python仍是JavaScript,咱們均可以用class來描述。能夠事先實現一個積木的基類,屬性裏能夠有積木的尺寸,圖片信息等等,方法能夠有包括圖片的加載。而具體的每一種積木則能夠繼承於基類,固然,繼承裏還能夠再加一層,好比咱們能夠給積木分一個類,好比計算類、運行結構類、數據類、輸出類、輸入類……積木類裏可能包含着其餘的積木對象,好比對於運算來講,多是別的兩個積木甚至更多的積木的參與。另外,每一個積木也能夠有一個resize或者reshape這樣的方法,由於形狀可能與它包含的其餘積木對象是相關的。固然,還得須要move這樣的方法。

  其次,咱們要爲當前全部的積木建模,能夠抽象出block(用來表示一段積木拼成的程序)和block_list(用來表示全部的block)這樣的類。block也有split/joint等方法,用於block的分裂、拼接。

  鼠標拖動積木,能夠對應於以上各類對象的操做。

  block_list裏記錄了全部的積木,以及積木與積木之間的鏈接關係,其實這些關係對於語言編譯、解釋來講,正是第一步parser但願獲得的結果。如今這些信息雖然不是經過語言編譯獲得,而是經過前端操做獲得,可是信息都完備了,那麼能夠直接使用這種鏈接關係一步一步獲得最終的結果。這種方式應該是最正常的一種方式。

 

  使用開發語言

  

  因而,最簡單的實現能夠是各個block映射成當前所用編程語言,再使用編程語言的eval函數來實現。不少語言都有eval函數,因此這個幾乎不是什麼問題。

  好比假設咱們的Scratch是用Python寫的,Scratch寫的質數程序能夠挨個積木來翻譯,獲得如下Python代碼:

var["i"] = 2
while not (var["i"]>10):
    var["k"] =  sqrt(var["i"])
    var["j"] = 2
    while not ((var["i"]>var["j"]) or ((var["i"]%var["j"])==0)):
        var["j"] += 1
    if (var["i"]>var["k"]):
        say(str(var["i"])+"是質數", 1)
    var["i"] += 1

 

  而後再在Python裏實現該實現的函數便可,好比這裏的say函數。

  以上的實現更爲簡單直接,實際上的確世面上是有商業軟件是這麼作的。特別是有的在線少兒編程教育,但願學生從積木編程過渡到具體計算機語言(通常仍是Python、JavaScript)的學習,會給一個積木編程和具體計算機語言之間的對應,對於教育而言,這應該是一個挺不錯的想法。可是這個也有必定的問題,由於這種對應學習通常也會作語言到積木方向的映射,這個自己就要作對語言的parser。而且積木編程和具體語言編程很難作到徹底合理的對應關係,特別是Python、JavaScript還支持面對對象、函數式編程,基本這些已經很難用積木的方式來表示了。

  再者,也很差教授單步、斷點這樣的調試手段。而對於上一種的方式,卻很容易作到支持這些調試方式。

 

  再造一種語言

 

  像上面同樣,仍是依次把積木映射成字符串,只是,不用原生的程序語言,而是再造一種語言。

  好比,上面的語言,可能被映射爲如下這樣的形式:

 

until >(i,100)
 set(k,sqrt(i))
 set(j,2)
 until or(>(j,k),=(%(i,j),0))
  add(j,1)
 end
 if >(j,k)
  echo(concatent(i,"是質數"))
 end
 add(i,1)
end

  上面的形式看起來基本全是函數調用形式,雖然與普通的寫法不同,可是由於沒有了中綴表達式,語法解析全是函數,由於函數的表示很是簡單,基本上數括號就行,因而很容易區分函數名和各個參數,很明顯使用遞歸很容易實現此處的函數調用,乃至本身寫一個棧來解析都很簡單。關鍵字此處只有until、if、end,從而能夠很方便的作個解釋器。

  特別的,既然本身給出語言的解釋器,天然能夠很容易的添加單步、斷點。

  固然,真的映射爲帶中綴表達式的天然也能夠,只是解釋器難度稍大一點罷了。

  Scratch採用的是另一種類似的方式,它將全部的數據用json來封裝,而描述上述代碼格式的是用以下一段數組,

[["whenGreenFlag"],
        ["setVar:to:", "i", "2"],
        ["doUntil",
                [">", ["readVariable", "i"], "100"],
                [["setVar:to:", "k", ["computeFunction:of:", "sqrt", ["readVariable", "i"]]],
                        ["setVar:to:", "j", "2"],
                ["doUntil",
                        ["|",
                        [">", ["readVariable", "j"], ["readVariable", "k"]],
                                ["=", ["%", ["readVariable", "i"], ["readVariable", "j"]], "0"]],
                        [["changeVar:by:", "j", 1]]],
                ["doIf",
                        [">", ["readVariable", "j"], ["readVariable", "k"]],
                        [["say:duration:elapsed:from:", ["concatenate:with:", ["readVariable", "i"], "是質數"], 1]]],
        ["changeVar:by:", "i", 1]]]]

  這段json的解析方式能夠與上述全函數的方式高度一致,天然也是高度一致的處理手段。

  固然,咱們也能夠相似下面這樣使用中綴表達式

until i>100
 k=sqrt(i)
 j=2
 until j>k or i%j==0
  j+=1
 end
 if j>k
  echo(i,"是質數")
 end
 i+=1
end

  中綴表達式涉及到運算符的優先級問題,處理起來相對比較複雜一些。固然,咱們也可使用LEX/YACC來寫parser,這樣會方便不少,linux下LEX通常使用flex,YACC通常使用bison。不過既然這裏的語言基本上只是作中間語言來使用,那麼作成這樣基本是沒有必要。

 

圖計算

 

  細緻的讀者應該能夠想到,既然爲圖形界面建模的過程當中,全部的積木之間的關係已是一個圖結構。而編程語言的編譯,目的就是爲了把編程語言變換成圖結構,既然圖結構如今已經有了,生成中間代碼看上去不過是多轉了一道彎而已,那咱們是否是能夠利用這個圖結構直接計算呢?

  實際上,固然,咱們是徹底能夠經過圖結構來作計算的。例如以下這樣的流程圖,咱們的數據結構中蘊含着流程圖的每一條邊(圖是有向圖,從而邊有方向)以及節點意義,程序員顯然都有直接計算的直覺。

   

 

   數據結構示意大體以下:

  Node:

    A : i<=1

    B(Switch) : i<100

    C : 打印i

    D : i<=i+1

  Entrance:

     A

  Edge:

    A->B

    B(False)->C

    C->D

    D->A

    B(True)->End

 

內部DSL式建構

 

  所謂內部DSL,則是以宿主語言爲基礎構建一種屬於本身的特定語言。這就意味着,新的語言實際上依然是宿主語言,只是在宿主語言基礎上構造了不少其餘的限制東西,好比讓其看起來很是相似於徹底獨立的語言。這是一種徹底不一樣於前面設計的方法的設計。

  而咱們也都知道,Lisp特別擅長幹設計DSL的事情,特別是Scheme,有多種手段能夠設計。好比咱們能夠用宏(Macro),Lisp的宏遠比C語言的宏要強大太多。雖然說宏是文字替換,但Lisp的宏支持遞歸,咱們徹底能夠用Lisp的宏來作反覆遞歸替換,因而咱們能夠用很是靈活的方式替換咱們的語法樹,甚至語法樹能夠用宏被替換的「面目全非」。Scheme還有神同樣的continuation,大部分DSL能夠設計爲面向過程的方式,而continuation就能夠很方便的爲面向過程的流控制來建模。以上兩個就已經可讓Scheme隨心所欲了,記得之前網上有個QBasic的Scheme實現就是利用的這個。而咱們固然也能夠再來考慮更通常的Scheme程序設計,利用算子中的閉包傳遞,咱們同樣能夠設計出好的內部DSL。

  咱們這裏並不打算用Scheme或者別的Lisp來說解,這裏依然用咱們經常使用的宿主語言來,好比Python。Python實際上是一種抽象度極高的語言,它比Scheme少的東西在我看來也就宏和continuation,再有就是尾遞歸沒有優化。

 

  咱們考慮如下這樣的一段程序,

repeat 3 (
  repeat 4 (
    display("=")
  )
  newline()
  display("test")
  newline()
)
display("The end")
newline()

 

  雖然,也許我在此並無說明這段程序的意思,身經百戰的你應該早已想出本程序的輸出多是

====
test
====
test
====
test
The end

  

  這裏repeat n (...)就表明着把括號裏面的內容執行n遍,而display函數則是把後面的內容打印出來,newline函數就是換行。這是一個最簡單的DSL了,可是在這個原理的基礎上咱們其實能夠作好比畫畫之類的程序,由於通常意義上的教孩子編程所用的畫畫不會構造過於繁瑣的圖形,從一開始就能夠思考出總體如何畫,因而利用循環就能夠了,而不須要用到判斷(然而複雜意義上的圖畫可能不是這樣,多是不斷的根據當前所畫內容決定以後所添加內容,這可能就得須要判斷)。

  Scratch中畫圖程序示例以下:

  

 

  結果就是畫個正六邊形以下: 

  

 

   和上述的DSL基本是一致的。

 

  但既然是想在Python上作內部DSL,咱們就得想一種方法嵌入到Python中,最天然的就是將以前的程序改寫就是以下:

run(
        repeat(3,
                repeat(4,
                        display("=")
                ),
                newline(),
                display("test"),
                newline()
        ),
        display("The end"),
        newline()
)

   既然沒有Scheme的Macro和Continuation,Python以上的方式也就是函數調用方式,只是把格式排了一下,加上縮進,從而比較容易看出語義。

 

  接下去考慮的就是具體如何實現:

  咱們先從display開始,第一個直覺是display和Python的print是同樣的。

  因而咱們就這樣設計了:

def display(s):
    print(s)

  看起來咱們日常的單個display都OK了,然而很快咱們就意識到如同repeat(2, display("test"))這樣的程序沒法正常工做,由於display打印以後並無產生別的影響,repeat天然無從知道display的動做,從而沒法把它的動做複製。

  從而display須要一點影響才能正常工做,一個方法就是經過變量傳遞出來,這是一個有反作用的手段,不少時候應該儘可能去避免。因而,咱們只考慮從display返回值來解決問題。也就是display函數須要返回display和display的參數的信息,newline也須要返回包含newline的信息,repeat也要返回repeat以及它全部參數的信息。

  最容易想到的固然是用字符串來表示上述信息。  

def display(a):
        return "display " + a

def newline():
        return "newline"

  而後咱們再接着寫repeat,

from functools import reduce
def repeat(times, *args):
        s = "repeat " + str(times) + '\n(\n'
        for arg in args:
                s += reduce(lambda a,b:a+b,map(lambda x:' '+x+'\n',arg.split('\n')))
        s += ")"
        return s

咱們運行一下

  print(repeat(2, display("test")))

  獲得

repeat 2
(
display test
)

  忽然意識到,這樣不對啊,如此繼續下去,run函數徹底成了一個語言的解釋器,這和咱們以前討論的徹底同樣。那咱們就不返回字符串,返回tuple、list之類的數據結構呢?換湯不換藥,仍是那麼回事,沒有什麼新意。

  

閉包構建

 

  迴避不了返回值要包含函數和函數參數的問題,只是,咱們能夠採用別的方式來作到,也就是閉包。

  所謂閉包,是一種算子,把函數的參數信息封進另一個函數,最終返回這個函數,如下舉一個簡單的例子就應該很明白了。

def addn(n):
    def f(x):
        return x+n
    return f

  或者用lambda,上述寫成

    addn = lambda n:lambda x:x+n

  使用的時候,好比我想獲得一個函數,返回輸入數獲得加1的結果,那麼addn(1)就是我所須要的函數。

  addn就是一個閉包。

 

  按照這個思路,咱們先想一想run函數如何寫,run的參數是一系列按前後順序要發生動做的函數,那麼run只是按順序執行這些函數。

def run(*args):
        for arg in args:
                arg()

 

  而後,咱們試圖編寫display和newline,代碼能夠以下:

def display(s):
        def f():
                print(s, end="")
        return f

def newline():
        def f():
                print("")
        return f

   

  咱們發現使用run、display、newline可讓運行結果徹底隨咱們的想法。

  接下來就是repeat。

  好比repeat(3, display("a"), newline()), 實際上應該是返回一個函數其執行結果是循環三次執行display("a")和newline()返回的函數,雖然開始有點拗口,但代碼並不太複雜:  

def repeat(times, *args):
        def f():
                for i in range(times):
                        for arg in args:
                                arg()
        return f

 

  因而,到此爲止,最簡單的只包含repeat、display、newline的DSL就已經完成了。

  咱們運行設計之初的代碼,的確獲得了想要的結果。

 

升級思路

 

  上面的這個DSL實在太簡單了,它雖然有循環,可是沒有引入變量,天然也就沒有引入運算,也不會有條件分支。

  那麼一切都從變量支持開始,變量意味着面向過程的程序在運行的過程當中,除了程序當前運行的位置以外,還有其餘的狀態。咱們以前repeat、display、newline返回的函數都是沒有參數的,這樣除了創造有反作用的函數,不然沒法攜帶其餘狀態轉變的信息。

  因而咱們能夠考慮用一個字典來表明程序中全部變量的狀態,而後讓全部的閉包最終都返回帶一個以這樣的表示變量的字典爲參數的函數。

  因而這種狀況下,run函數應該這樣寫:

def run(*args):
        var_list={}
        for arg in args:
                var_list = arg(var_list)

  上面的run_list就是咱們所須要的表明全部變量的字典。

  在此基礎上,咱們能夠構造用來設置變量的函數和得到變量的函數:

def set_value(k, v):
        def f(var_list):
                var_list[k] = to_value(v, var_list)
                return var_list
        return f
def get_value(k):
        return lambda var_list : var_list[k]

  重寫display和newline,返回函數的參數中固然應該添加var_list,雖然二者不是與變量直接關聯,但也彷佛只須要保證把var_list直接返回,以確保run以及別的閉包調用的正確便可。

def display(s):
        def f(var_list):
                print(s, end="")
                return var_list
        return f

def newline():
        def f(var_list):
                print("")
                return var_list
        return f

   然而,咱們實際上並未細想,display後面接的若是是常量的話,上述的實現並無錯誤,但若是不是常量呢?好比display(get_value("i")),get_value("i")是一個函數,上述的print顯然不能把但願打印的值打印。

  因而咱們按理應該在此判斷一下display的參數s,若是是函數,應該先求值一下再打印。

def display(s):
        def f(var_list):
                if type(s)==type(lambda x:x):
                        print(s(var_list),end="")
                else:
                        print(s, end="")
                return var_list
        return f

  上述判斷s的類型是函數,也可使用

  from types import FunctionType

  isinstance(a, FunctionType)

  上述咱們能夠測試一段代碼

run(
        set_value("i", 1),
        repeat(3,
                display(get_value("i")),
                newline()
        )
)

  運行結果是

1

1

1

  與咱們想象的一致,從而基本驗證其正確性。

 

  固然,這還只是拋磚引玉一下,並無涉及到計算、條件分支、條件循環以及continue、break,乃至於goto,甚至於變量做用域、函數等。甚至於,在這個模式上咱們甚至還能夠加上調試模式,加入單步、斷點等調試手段。

  對於具體作法此處再也不展開,讀者能夠自行思考。

  開頭的程序

  

  引入的東西相對於文中多一些,咱們如今終於也有了一點眉目。程序代碼詳細以下面文件中所示,恕不細說。

                                                                                                           

 

少兒編程教育的思考

 

  一個致力於少兒編程教育的朋友跟我聊天,說到新接手的一個班,雖然以前都在另外一少兒編程平臺下學習了近一年,但卻連最基本的編程邏輯都不懂。雖然少兒編程教的通常是面向過程的編程,但是班上沒有一個小朋友能夠理解流程圖這個東西。質數判斷本應是個很簡單的問題(固然,咱們先不深刻的看待此問題),然而甚至於他在班上把詳細的過程描述清楚了,小朋友也會按照過程用紙筆來判斷,但是一上程序全班沒有一我的能夠寫出來。這位朋友很生氣,以爲連流程圖都不歸入教學體系是個很過度的事情,由於流程圖是面向過程編程的基本抽象。小朋友最終連這麼簡單的程序都編不出來只能說明以前的教學簡直就是應付,甚至欺騙。

  而我卻是跟他大談或許教學目的該爲如何教會小朋友一步步學會本身製做少兒編程工具,固然多是針對對編程很是感興趣的孩子。現實是,這樣的孩子少,能夠這樣教的老師也少,從而沒法產生合理的商業利益。因而,如何教好更多的孩子纔是他所認爲的教育之重。

  我一貫認爲老師教學生,不是複製本身,而是教會學生思考的方法。孩子學會思考的方法遠比手把手的作個老師的克隆更強,獨立作出一個程序,即便再爛,其價值遠超過老師全力指導下寫的高深程序。

  因而,仍是應該把目光放回到咱們目前教學的工具上,如何讓孩子真正的理解程序,倒未必一直要從純數學出發,也不須要什麼高深算法,什麼函數式編程,先掌握面向過程的精髓吧,讓孩子能夠自由的產生想法,再由想法變成代碼。時機成熟了,再告訴孩子,某些東西能夠有,好比算法,好比各類編程範式。當年日本圍棋,超一流棋手的搖籃木穀道場,老師對於學生不按老師的想法行棋都是要懲罰的,然而六大超一流棋手風格各異,並無按照老師的手段來行棋。換句話說,教育中,傳承是根,但挖掘潛力才更爲重要。

相關文章
相關標籤/搜索