就在昨天我完成了Go版本redux的主要功能
發文要附圖(誤),是連結: redux
首先redux是什麼呢?redux的起源與react有關,facebook推出一種叫flux的資料流架構,而如今redux憑借著簡單優雅的架構成為了主流的實作,它受到elm這隻程式語言的啟發而對flux做出修正
那麼flux是什麼呢?或者說,它的重點是什麼?flux試圖解決JavaScript程式長久以來面臨的問題:究竟是誰改變了狀態?
事實上這不只是JavaScript會面臨的問題,是所有具有多個改變狀態的原因的程式需要面對的問題
追蹤狀態的改變是如此的複雜,flux點出的關鍵在於我們不知道狀態存放的地點以及誰可以接觸到它,既然了解了問題點所在,我們就能問題提出解法
好吧!到了這裡,相信大家都已經了解flux存在的意義,那麼它面臨了什麼樣的新問題呢?
問題在於實作,flux由action dispatcher store view組成(但是view通常並非其實作所關注的,而是一種抽象的概念,指涉使用它的展示層)
store儲存資料,dispatcher分發事件,view觀察store中的資料對自己做出更新
使用者觸發action,dispatch就會觸發已註冊的那些callback,最終達到update store的效果
實際操作起來的問題是dispatcher會變得非常多,每次應用就會出現類似的程式
而這顯然與程式工程師們習慣不符,我們就是『懶』
那麼如何解決這樣的問題呢?redux參考函數式語言的一些概念,提出了
reducer(previousState, action) => newState
這樣的等式,更有趣的是你不能提供一個store初始狀態給redux,而是要給予一個個reducer組成store,每個reducer各自擁有initial state,store的初始狀態變由此決定
除此之外,redux也沒有dispatcher,它只有dispatch函數,所以我們不用管理事件會分發給誰,因為所有reducer都會收到
接著是subscribe函數,這個函數讓呼叫方的函數可以在dispatch時自動被執行,唯一的限制是裡面不能再度呼叫dispatch,這會無限遞迴
好了,最後我們進入正題,這個Go版的redux究竟是怎麼實作的呢?
首先是NewStore函數,它是我們這個版本的createStore,命名是依照Go的慣例
其回傳一個Store指標(我們當然不想要複製整個Store,其成本難以想像,JS的物件則是本來就不會複製(預設))
接受reducer型別作為參數
reducer定義非常簡單,就是我們前面看到的reducer(previousState, action) => newState的樣子
Store定義如下
reducers, subscribes無須解釋,GetState即是我們所有state存放的位置,這樣取名是為了呼叫時的清晰度
(其實想改掉了,這樣會被客戶端修改,其實失去了保證性)
mu在共時程式之中保證Dispatch會安全完成
atSubscribe則是保證Subscribe中不得呼叫Dispatch
將參數分成兩部份是因為這樣就不需要自己檢查參數(...散列可以為空之特性),而是由編譯器做出保證
除去上述的特殊設計,非常容易看出程式的邏輯,我們把一個個reducer綁上我們可愛的store
到這裡我們先看看實際案例
我們居然可以GetState["counter"]!?
這就是用在newReducer中的魔法了
應該很容易能看出,我們找出reducer的參考名稱,並用此名稱作為鍵值,用nil調用reducer(Action在這裡不重要,想一下Go版reducer的定義中必須在nil時回傳initial state)
然後將reducer放入我們的大殺器reducers中(!?)
getReducerName實作如下
我們先用runtime API取得指標指向的函數,再存取其名稱,這時我們會得到完整的名稱(套件.參考名稱)
所以去除套件部份之後就是我們想要的部份了
做成helper函數的原因是之後更新state時也需要這個函數,DRY,很好
接著我們看Dispatch的實作
可以看到if atSubscribe程式就會崩潰,這樣能夠阻止想做蠢事的正常呼叫(但是你阻止不了硬要用recover卻不處理這個問題的人)(目前這部份有bug,事實上我們會先遇上deadlock而不是panic,雖然結果正確但是失去錯誤訊息的提示)
我們對操作上鎖
所以程式能夠安全的在共時程式中執行,而狀態更新上鎖也算是正常的設計
然後我們執行那些subscribe進來的函數,注意註冊的函數不能有參數,因為我也不知道要傳什麼參數給你,那很不合理對吧
而最後Subscribe的實作索然無味,就只是將我們可愛的函式們放入註冊函數集合中
最後總結是為了Go的一些特性我們得要做出取捨,例如原版中,只有一個reducer的情況只需要使用getState就能得到狀態,但是我統一使用函數名稱做存取,因為Go不允許多載函數,而動態參數列亦不太適合(我們得在參數數量超過1時panic,0時檢查reducer的數量,都很麻煩)
另外state型別為interface{}的部份,這並不會造成任何問題,因為state只在reducer中被使用,因此強制轉型並不會造成問題
謝謝觀看,歡迎提出改進意見
2017年12月3日 星期日
2017年11月30日 星期四
Go object orient
Go語言可以說是獨樹一幟,它採取了比較有趣的語法來表現物件
將資料與方法分離,類似於C語言
事實上你可以看做第一個參數為Foo指標的函數,只是Go有語法糖可以讓我們用 . 運算子存取方法
有趣的地方在於你還可以接收Foo型別而非Foo指標型別,如此一來便會複製呼叫物件後執行函數,但這通常並非常見作法,沒有理由時請用指標
而struct的建構有一個特點,如果指定名稱,則剩餘欄位將自動補零值(Go對此做盡可能保證)
因此標準風格也建議總是指定名稱
!注意跨行必須有逗號o
Go有所謂的interface,這是一種特殊的型別,它接受實作它的型別作為右值
而所謂的實作就只需要寫一個一樣的方法
值得注意的是,Go語言的新手往往習慣性的宣告struct與interface
但其實這是不需要的
因為Go接受類似C的匿名struct,而interface亦同(也可以說臨時struct,端看你習慣的稱呼)
重要的是,往往該結構只使用一次而已,宣告全新的型別是污染命名空間,尤其在測試程式中經常遇到此種清況
簡而言之,Go提供了一個異於傳統的設計思維,如何取捨就看你的智慧了
將資料與方法分離,類似於C語言
type Foo struct { Name string } func (f *Foo) SetName(newName string) { f.Name = newName }可以看出其結構的簡潔
事實上你可以看做第一個參數為Foo指標的函數,只是Go有語法糖可以讓我們用 . 運算子存取方法
有趣的地方在於你還可以接收Foo型別而非Foo指標型別,如此一來便會複製呼叫物件後執行函數,但這通常並非常見作法,沒有理由時請用指標
而struct的建構有一個特點,如果指定名稱,則剩餘欄位將自動補零值(Go對此做盡可能保證)
因此標準風格也建議總是指定名稱
Foo { Name: "foo", }這就是所謂的指定名稱
!注意跨行必須有逗號o
Go有所謂的interface,這是一種特殊的型別,它接受實作它的型別作為右值
而所謂的實作就只需要寫一個一樣的方法
type FooI interface { SetName(string) }Foo有實作此介面,就是這麼簡單
值得注意的是,Go語言的新手往往習慣性的宣告struct與interface
但其實這是不需要的
因為Go接受類似C的匿名struct,而interface亦同(也可以說臨時struct,端看你習慣的稱呼)
重要的是,往往該結構只使用一次而已,宣告全新的型別是污染命名空間,尤其在測試程式中經常遇到此種清況
簡而言之,Go提供了一個異於傳統的設計思維,如何取捨就看你的智慧了
2017年11月18日 星期六
Swift -- extension skill, impl policy base design
經過幾個禮拜的思考,我認爲jon hoffman先生說得有理,我們應該傾向簡單的設計,而Swift不是C++,因此不應該試圖模仿
回到Poilcy的設計上,我們最初的期望是:在編譯期選擇型別,並且透過隱式約束,而不是顯式
先看看C++的案例
因此我們不會有執行期(runtime)成本,且使用時檢查此點非常重要,這是我們之後會在Swift遇到的難題
那麼我們要如何在Swift中實現一樣的能力呢?
首先我們要了解到Swift並非在使用時檢查,而是宣告時檢查,這只得剛才的程式寫法完全無法編譯通過:首先編譯器會告訴你DBImpl沒有建構式(在C++,如果你沒有宣告建構式,編譯器會幫你寫一個),接著編譯器認為DBImpl沒有connect方法
我最開始的解決方案是利用constraint讓編譯器得到這些資訊
可是這樣一來,程式就變得沒有彈性,而且這樣搞,寫多型不就好了(我們只享受到一點效能優化,但是沒有得到最重要的彈性)
接下來我學習到where clauses這個特性,我終於找到了正確的實作方式
但是卻有很明顯的重複性,我們有幾個Policy就需要extension幾次,且所做的事都差不多,只能說Swift不是C++,XD
回到Poilcy的設計上,我們最初的期望是:在編譯期選擇型別,並且透過隱式約束,而不是顯式
先看看C++的案例
struct SQLServer { void connect(); // ... } struct MySQL { void connect(); // ... } class DB<DBImpl> { public: void connect() { DBImpl().connect(); } }我們建構泛型DBImpl的實例並且呼叫其方法,我們知道C++的樣板會在"使用"時展開並檢查
因此我們不會有執行期(runtime)成本,且使用時檢查此點非常重要,這是我們之後會在Swift遇到的難題
那麼我們要如何在Swift中實現一樣的能力呢?
首先我們要了解到Swift並非在使用時檢查,而是宣告時檢查,這只得剛才的程式寫法完全無法編譯通過:首先編譯器會告訴你DBImpl沒有建構式(在C++,如果你沒有宣告建構式,編譯器會幫你寫一個),接著編譯器認為DBImpl沒有connect方法
我最開始的解決方案是利用constraint讓編譯器得到這些資訊
protocol DBPolicy { init() func connect() } class SQLServer : DBPolicy { required init() { ... } func connect() { ... } // ... } class MySQL : DBPolicy { required init() { ... } func connect() { ... } // ... } class DB<DBImpl: DBPolicy> { func connect() { DBImpl().connect() } }
接下來我學習到where clauses這個特性,我終於找到了正確的實作方式
class SQLServer { required init() { ... } func connect() { ... } // ... } class MySQL { required init() { ... } func connect() { ... } // ... } class DB<DBImpl> { ... } extension DB where DBImpl == MySQL { func connect() { MySQL().connect() } }這個方案與我們所想像的差不多,確實符合了隱式約束以及編譯期選擇
但是卻有很明顯的重複性,我們有幾個Policy就需要extension幾次,且所做的事都差不多,只能說Swift不是C++,XD
2017年11月12日 星期日
學習做一個編譯器應該怎麼開始?
我個人的學習經驗是先去找一個你用過的語言,例如C++或是Go之類的
然後找由它們實作的編譯器原始碼觀察,大致應該都能找到 lexer parser codegen(or execute-enigne)這些部份
接著找本基礎書籍來看(我推薦龍書或是Parr的程式語言實作模式)
一一對照實際程式跟理論怎麼接合的,一般來說在書裡通常只找得到殘缺的實際案例
所以找到一個優秀的實作非常重要,我認為Go本身的原始碼就非常值得閱讀
另外也有一些比較簡單的實作(如果你害怕一個大規模的編譯器很難追蹤跟看懂)
例如用Go實作的gisp,用Rust的dyon
以上專案都值得看過
Lexer的部份,我認為實作BrainFuck是非常適合的練習,我也有用Nim做了一個
因為狀態比較少,適合你專注於整體概念上,而不是被複雜的細節繞到暈頭轉向
Parser的部份我認為還是手刻比較方便,並不是說我不用工具,事實上我通常用工具
不過你是要學習,我認為工具對這階段來說並沒有意義
這部份可以嘗試寫出四則運算式的解析器
func parseDefine() *VarDefine {
name := lex.Get()
if name.Typ == ID {
if lex.Get().Typ == ASSIGN {
return &VarDefine{
name: name,
expr: parseExpr(),
}
}
}
}
上面的函式可以觀察出如何透過遞迴調用產生AST
AST? 我聽到你的疑問了
所謂AST就是抽象語法樹的意思,透過複數的AST互相包含(上述的變數宣告就包含運算式)抑或連貫(變數宣告就是連貫的)成為一支原始程式的中間表述法,進而簡化翻譯為目標語言的難度
因為我們可以宣告方法處理AST資料結構,然後迭代處理整個AST樹
例如函數的AST可能可以這樣定義
type FunctionAst struct {
name string
functionType TypeAst
statements StatementsAst
}
func (f *Function) Codegen(module Module) {
f := createFunction(module, name, functionType.Codegen(module))
block := statements.Codegen(module)
Insert(f, block)
}
可能會像這樣,不斷深入下一層的AST,直到完成翻譯工作
至於優化工作,那又是新的篇章了,期待下次介紹
然後找由它們實作的編譯器原始碼觀察,大致應該都能找到 lexer parser codegen(or execute-enigne)這些部份
接著找本基礎書籍來看(我推薦龍書或是Parr的程式語言實作模式)
一一對照實際程式跟理論怎麼接合的,一般來說在書裡通常只找得到殘缺的實際案例
所以找到一個優秀的實作非常重要,我認為Go本身的原始碼就非常值得閱讀
另外也有一些比較簡單的實作(如果你害怕一個大規模的編譯器很難追蹤跟看懂)
例如用Go實作的gisp,用Rust的dyon
以上專案都值得看過
Lexer的部份,我認為實作BrainFuck是非常適合的練習,我也有用Nim做了一個
因為狀態比較少,適合你專注於整體概念上,而不是被複雜的細節繞到暈頭轉向
Parser的部份我認為還是手刻比較方便,並不是說我不用工具,事實上我通常用工具
不過你是要學習,我認為工具對這階段來說並沒有意義
這部份可以嘗試寫出四則運算式的解析器
func parseDefine() *VarDefine {
name := lex.Get()
if name.Typ == ID {
if lex.Get().Typ == ASSIGN {
return &VarDefine{
name: name,
expr: parseExpr(),
}
}
}
}
上面的函式可以觀察出如何透過遞迴調用產生AST
AST? 我聽到你的疑問了
所謂AST就是抽象語法樹的意思,透過複數的AST互相包含(上述的變數宣告就包含運算式)抑或連貫(變數宣告就是連貫的)成為一支原始程式的中間表述法,進而簡化翻譯為目標語言的難度
因為我們可以宣告方法處理AST資料結構,然後迭代處理整個AST樹
例如函數的AST可能可以這樣定義
type FunctionAst struct {
name string
functionType TypeAst
statements StatementsAst
}
func (f *Function) Codegen(module Module) {
f := createFunction(module, name, functionType.Codegen(module))
block := statements.Codegen(module)
Insert(f, block)
}
可能會像這樣,不斷深入下一層的AST,直到完成翻譯工作
至於優化工作,那又是新的篇章了,期待下次介紹
Redux in go
剛才利用一點時間寫了一個Go版本的Redux
https://github.com/dannypsnl/redux
發現一些有趣的設計議題
我們先看到一個標準Redux程式(JS版本)
我們來看看Go版本(現在的實現)
第一,不能有型別,有點尷尬對吧
沒有型別我們就要非常小心的確保我們在做什麼,以避免錯誤的轉型
第二,我們沒有辦法設定初始值,這下就有趣了,目前我是先強制給0才能運作,這當然是不對的,下一步應該會調整這部份的API,當然還是會盡量保持與Redux本身相同
至於第一個問題,其實只是麻煩而已,只要回到定義Reducer的地方,我們還是能夠取得正確的型別資訊,當然如果Go擁有泛型,我們今天就不用這麼麻煩了
關於泛型,我覺得重點在於它在參數之前完成計算,就是最重要的特點
如果獲得泛型,確實能夠解決我們現在遇上的麻煩,如何匹配不同型別的state
但是也會帶來別的麻煩,當然這又是另一個故事了...
https://github.com/dannypsnl/redux
發現一些有趣的設計議題
我們先看到一個標準Redux程式(JS版本)
function counter(state = 0, action) { switch (action.type) { case 'INCREMENT': return state + 1 case 'DECREMENT': return state - 1 default: return state } } let store = createStore(counter) store.dispatch({ type: 'INCREMENT' })Redux設計思想是藉由回傳新的狀態取代改變手上的狀態來進行狀態管理
我們來看看Go版本(現在的實現)
func counter(state interface{}, act redux.Action) interface{} { switch act.Type { case "INC": return state.(int)+1 case "DEC": return state.(int)-1 default: return state } } func main() { store := redux.NewStore(counter) store.Dispatch(redux.SendAction("INC")) fmt.Printf("Now state is %v\n", store.GetState()) }在API上我沒有特別設計,所以我發現兩個問題
第一,不能有型別,有點尷尬對吧
沒有型別我們就要非常小心的確保我們在做什麼,以避免錯誤的轉型
第二,我們沒有辦法設定初始值,這下就有趣了,目前我是先強制給0才能運作,這當然是不對的,下一步應該會調整這部份的API,當然還是會盡量保持與Redux本身相同
至於第一個問題,其實只是麻煩而已,只要回到定義Reducer的地方,我們還是能夠取得正確的型別資訊,當然如果Go擁有泛型,我們今天就不用這麼麻煩了
關於泛型,我覺得重點在於它在參數之前完成計算,就是最重要的特點
如果獲得泛型,確實能夠解決我們現在遇上的麻煩,如何匹配不同型別的state
但是也會帶來別的麻煩,當然這又是另一個故事了...
2017年10月24日 星期二
Vim replace
在Vim中
找出文字並修改不是一件複雜的事
你只需要
:Start, Ends/You want to replace/You want to get/g
:是進入命令模式
Start和End都是行號的代號,你可以用數字1, 2, 3...
也可以是特殊字元,例如$是最後一行
接著s///g是取代命令,s/你想取代的/你想顯示的新字串/g
字串使用正規語言進行比對
例如:
[a-zA-Z]+
\".*?\"
等等
最後,如果想要一次套用多個命令
使用 | 字元來分隔不同命令
例如:
:1,$s/Care/Car/g | 1,$s/Circlew/Circle/g
找出文字並修改不是一件複雜的事
你只需要
:Start, Ends/You want to replace/You want to get/g
:是進入命令模式
Start和End都是行號的代號,你可以用數字1, 2, 3...
也可以是特殊字元,例如$是最後一行
接著s///g是取代命令,s/你想取代的/你想顯示的新字串/g
字串使用正規語言進行比對
例如:
[a-zA-Z]+
\".*?\"
等等
最後,如果想要一次套用多個命令
使用 | 字元來分隔不同命令
例如:
:1,$s/Care/Car/g | 1,$s/Circlew/Circle/g
2017年10月16日 星期一
實作http框架by Go ---- Rocket
最近吃飽閒閒就寫了一個Go的伺服器框架
靈感來自Rust的一個框架 rocket.rs
當然Go與Rust有許多的相異之處,例如Go完全沒有泛型,也沒有巨集等等
因此兩個框架大概只有皮像吧
Rocket for Go大致上看起來像是這樣(!!! 這是舊API)
import ( "fmt" "github.com/dannypsnl/rocket" ) var hello = rocket.Handler { Route: "/name/:name/age/:age", Do: func(Context map[string]string) string { return fmt.Sprintf("Hello, %s\nYour age is %s", Context["name"], Context["age"]) }, } func main() { rocket.Ignite(":8080"). Mount("/hello", hello). Launch() // Start Serve }
對比一下Rust的原版
#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] extern crate rocket; #[get("/<name>/<age>")] fn hello(name: String, age: u8) -> String { format!("Hello, {} year old named {}!", age, name) } fn main() { rocket::ignite().mount("/hello", routes![hello]).launch(); }
看起來差不多
那麼這一切是怎麼實現的呢?
最開始我直接做了一個簡單的對應,把rocket.Handler轉換成Go http程式庫的HandleFunc能夠接受的格式中處理
大概像這樣
func (r *Handler) toHandleFunc() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "%v\n", r.Do()) } }
不過很快就發現原生的http程式庫並不支援動態路由(也就是/:name等可部份替換的路由)
最後敲定的解決辦法是用regexp套件進行路由的比較,以取得對應的Handler
因此我們要能夠在註冊程式取得路由的regex以及參數
func (r *Rocket) Mount(route string, h Handler) *Rocket { route += h.Route match, params := splitMountUrl(route) h.params = params r.matchs = append(r.matchs, match) r.handlers[match] = h return r }
這就是Mount的完整實現,註冊路由由兩個部份組成,一個是Mount函式自帶的路由,另一部份是Handler擁有的路由
結合之後用splitMountUrl這個輔助函式切分出路由的regex模式以及參數
接下來將參數有哪些告訴handler
然後將regex模式放進全域中的regex模式陣列中
並且將regex模式與handler對應起來
那麼splitMountUrl究竟是如何實現的呢?
const legalCharsInUrl = "([a-zA-Z0-9-_]+)" func splitMountUrl(route string) (string, []string) { var match string var params []string // '/:id' is params in url. // '/*filepath' is params about filepath. // '/home, data' is params from post method. for _, url := range strings.Split(route, "/") { if strings.HasPrefix(url, ":") { match += "/" + legalCharsInUrl params = append(params, url[1:]) } else if strings.HasPrefix(url, "*") { match += "/.*?" params = append(params, url[1:]) break } else if url != "" { match += "/" + url } } if match == "" { match = "/" } return match, params }
這是追求簡單化的實作,非常沒有效率
但是暫時就這樣了
ps. 第三種對應我還未實作
首先用/字串切分路由,接著如果是:開頭的就是參數部份,我們就插入一個legalCharsInUrl字串
這個字串是我們能在路由中接受的字元集的regex表示法
那麼*則是路徑參數,那麼之後的路由都屬於路徑,因此不再處理
最後是!= ""是因為Split後的陣列會有空字串,這會導致錯誤,因此略過
接著我們看Launch的實作
func (rk *Rocket) Launch() { http.HandleFunc("/", rk.ServeHTTP) log.Fatal(http.ListenAndServe(rk.port, nil)) }
簡單到不行
我們提交唯一的HandlerFunction(原生http程式庫的handler)
然後啟動伺服器
最後來到匹配路由的核心部份
rocket.ServeHTTP
func (rk *Rocket) ServeHTTP(w http.ResponseWriter, r *http.Request) { var match string for _, m := range rk.matchs { // rk.matchs are those static routes if m != "/" { matched, err := regexp.MatchString(m, r.URL.Path) if matched && err == nil { match = m break } } else { if m == r.URL.Path { match = m } } } fmt.Printf("Rquest URL: %#v\n", r.URL.Path) h := rk.handlers[match] matchEls := strings.Split(match, "/") Context := make(map[string]string) splitRqUrl := strings.Split(r.URL.Path, "/") j := 0 for i, p := range splitRqUrl { if matchEls[i] == legalCharsInUrl { Context[h.params[j]] = p j++ } else if matchEls[i] == ".*?" { Context[h.params[j]] = strings.Join(splitRqUrl[i:], "/") break } } fmt.Fprintf(w, h.Do(Context)) }
輪詢rocket中的matchs(還記得吧,我們在Mount中看過)
去除/是因為在正規表達式中它會匹配走所有路由,而那不是我們預期的行為
其餘路由如果成功(matched==true)匹配到request.URL.Path,就把match變數改成這個regex
因為接著我們要靠這個值取得對應的handler(rocket.handlers[match])
接著我們把match與r.URL.Path用Split切開,如果是match的子字串是 ':' 也就是(legalCharsInUrl)
那麼就把對應的splitRqUrl的值放入
例如:
match: /hello/name/:name/age/:age
r.URL.Path: /hello/name/danny/age/20
其Context就是
{
"name": "danny",
"age": "20"
}
至於 '*' 也就是 ".*?" 則是把接下來的整段路由都放入Context,這需要用Join恢復被切開的字串
最後將Context傳入Handler.Do並寫出其執行結果
這就是Rocket for Go用到的所有技術
最後這裡 => Rocket for Go
就是專案原始碼所在的位置
訂閱:
文章 (Atom)