顯示具有 Redux 標籤的文章。 顯示所有文章
顯示具有 Redux 標籤的文章。 顯示所有文章

2018年1月1日 星期一

Refactor Redux-go

如標題所示,這次我要談論的是redux-go重構
在幾次的測試撰寫過程中,我發現舊版的redux-go有一些小問題
首先是Action物件建構不易,且無關的資訊不斷出現
何謂建構不易?我們必須用
redux.SendAction("type")
這樣彆扭的寫法來建構只有typeAction
然而需要Args時,卻要使用
&redux.Action{
    Type: "type",
    Args: map[string]interface{} {
        // ...
    },
}
這樣複雜的寫法,直接使用struct非常容易出錯,如果客戶端直接插值給Args,結果只會是panic
讓介面容易被使用,不容易被誤用
而且redux這個模組資訊完全是非必要的,所以我在v0.5.0將整個Action部份移到redux/action模組
這樣一來程式就變成了
import "github.com/dannypsnl/redux/action"

action.New("type")
非必要的資訊已被移除,現在我們可以專注於action
下一步是讓新增Args的介面容易使用,所以我在v0.5.1新增func (*Action) Arg(string, interface{})方法進入action模組
於是新增Args變成
action.New("type").
    Arg("key", value).
    Arg("key2", value)
這樣的流暢呼叫式

第二個部份是Store,大抵和Action問題一樣,所以亦將其移入store模組
所以客戶端程式就變成了
import "github.com/dannypsnl/redux/store"
import "github.com/dannypsnl/redux/action"

func main() {
    store := store.New(reducer...)
    store.Dispatch(
        action.New("type").
            Arg("key", value).
            Arg("key2", value))
}
注意省略概念外的程式

第三是DispatchC方法的部份(屬於Store)
原本我發現在效能較高的電腦上,序列式的計算subscribed function比共時化的程式更快
於是分成兩個API
但是經過檢討,我認為這只會造成困惑,且未來多核心能力上升,Go亦可能優化其排程器使實作更加高效,比起讓客戶端負擔測試程式效能的成本,直接採取共時版本的實作更加適合,因此採取這個作法(v0.6.0)

經過這次重構,我重新認識了Go的模組組織概念,還有誰來負擔決策成本的問題,是非常有趣的體驗

2017年12月10日 星期日

React with Redux -- todo list

Everyone know todo list is a good practice at front-end programming.
I also create one, and notes some interesting points.
Let's start it.
First, download create-react-app by npm.
$ npm i -g create-react-app
Then use it create a new project `react-todo`
$ create-react-app react-todo
Waiting few seconds. Then you will see a directory call `react-todo` at there.
Ok, `cd src/` then you can find App.js.
We don't deal other things. Focus on todo list. So let's insert some html in App component!
<ul>
  <li>todo 1</li>
  <li>todo 2</li>
  <li>todo 3</li>
</ul>
You can see the result by execute `npm start`.
I usually use two checker. `flow` & `npm test`. `npm test` is provided by create-react-app. It using jest(when I write this).
And `flow` is a type checker. It can check type for you, and best thing is you can choose you want to use it or not, by adding comment at the file's head:
/*
@flow
*/
to open, or close it by no adding these.
Ok, so now we know this part is todo-list. Good, I use component TodoList instead of hard code at here first.
<TodoList />
$ mkdir TodoList/ && cd TodoList/ && touch index.js
Then `import TodoList from './TodoList'` to get component.
It doesn't hard. Next we that TodoList can work.
class TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      todos: [], // string[]
      todoStr: "" // string
    }
  }
  // evt of nextTodo is Input Element's event, listen to OnChange
  nextTodo = (evt) => {
    this.setState({
      todoStr: evt.target.value
    });
  };
  // evt of addTodo is button Element's event, listen to OnClick
  addTodo = (evt) => {
    this.setState({
      todos: [...this.state.todos, this.state.todoStr]
    })
  }
  render = () => (
    <div>
      <input value={this.state.todoStr} onChange={this.nextTodo}/>
      <button onClick={this.addTodo}>Add Todo</button>
      <ul>{
        this.state.todos.map((todo, index) => <li key={index}>{todo}</li>)
      }</ul>
    </div>
  )
}
It works. Then you can trying to spread out Todo Component, if you need a more complex Todo component.
Then we go to see redux.
import { createStore } from "redux";

type TodoAction = {
  type: string,
  index?: number,
  todoStr?: string
};

const todocer = (state: string[] = [], action: TodoAction) => {
  switch (action.type) {
    case "ADD_TODO":
      return [...state, action.todoStr];
    case "DELETE_TODO":
      return state.filter((todo, index) => index !== action.index);
    default:
      return state;
  }
};

export default createStore(todocer);
It's a little bit complex, because I use flow in the example here, but still, you can see we are doing the same things. But we move TodoList's data structure management from TodoList component to our reducer todocer. Why we have to do this? Because I want to talk about redux? I am joking. The truth is if our project grow, and other part depends on todo-list's status to decide what should them do. In traditional way, we will go to a hell about data dependencies, a ref b, b ref c... And finally, infinite task be created, but you almost can not find out the begin of this chaos.
Redux didn't solve the problem, but it that flow of data changing is visible & traceable,
so on we can find out the problem instead of rewrite it! Oh! I forgot everyone rewrite front-end every six months. Good!
Next we use Provider connect react & redux, you can without it! That's true, I usually use import store from 'path/to/store' to manage access, but today let's use react-redux.
import { Provider } from "react-redux";
import store from "path/to/store";

<Provider store={store}>
  <App />
</Provider>
This is how we use react-redux, it will send store by context to every child component.
So you will get the right to access the store you put into Provider by
this.context.store
ps. I don't want to talk about redux's API, you can google it.
I think this trick cause some problem, we have to import a lot of thing when testing, and that is boring. And connect function is too complex to hard to learn how to use. Of course, once you got the point, that will be easy.
Today's article end at here, thanks for read.




Ok

2017年12月3日 星期日

Redux in Go

就在昨天我完成了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型別作為參數
type reducer func(interface{}, Action) interface{}
reducer定義非常簡單,就是我們前面看到的reducer(previousState, action) => newState的樣子
Store定義如下
type Store struct {
        GetState map[string]interface{}
        reducers []reducer
        subscribes []func()
        atSubscribe bool
        mu sync.Mutex
}
reducers, subscribes無須解釋,GetState即是我們所有state存放的位置,這樣取名是為了呼叫時的清晰度
(其實想改掉了,這樣會被客戶端修改,其實失去了保證性)
mu在共時程式之中保證Dispatch會安全完成
atSubscribe則是保證Subscribe中不得呼叫Dispatch
func NewStore(r reducer, reducers ...reducer) *Store {
        s := &Store{
                GetState:    make(map[string]interface{}),
                atSubscribe: false,
        }
        s.newReducer(r)
        for _, r := range reducers {
                s.newReducer(r)
        }
        return s
}
將參數分成兩部份是因為這樣就不需要自己檢查參數(...散列可以為空之特性),而是由編譯器做出保證
除去上述的特殊設計,非常容易看出程式的邏輯,我們把一個個reducer綁上我們可愛的store
到這裡我們先看看實際案例
import "github.com/dannypsnl/redux"

func counter(state interface{}, action redux.Action) interface{} {
        // Initial State
        if state == nil {
                return 0
        }
        switch action.Type {
        case "INC":
                return state.(int)+1
        case "DEC":
                return state.(int)-1
        default:
                return state
        }
}

func main() {
        store := redux.NewStore(counter)
        store.Subscribe(func() {
                fmt.Printf("Now state is %v\n", store.GetState["counter"])
        })
        store.Dispatch(redux.SendAction("INC"))
}
我們居然可以GetState["counter"]!?
這就是用在newReducer中的魔法了
func (s *Store) newReducer(r reducer) {
        s.GetState[getReducerName(r)] = r(nil, Action{})
        s.reducers = append(s.reducers, r)
}
應該很容易能看出,我們找出reducer的參考名稱,並用此名稱作為鍵值,用nil調用reducer(Action在這裡不重要,想一下Go版reducer的定義中必須在nil時回傳initial state)
然後將reducer放入我們的大殺器reducers中(!?)
getReducerName實作如下
func getReducerName(r reducer) string {
        fullName := runtime.FuncForPC(reflect.ValueOf(r).Pointer()).Name()
        return fullName[strings.LastIndexByte(fullName, '.')+1:]
}
我們先用runtime API取得指標指向的函數,再存取其名稱,這時我們會得到完整的名稱(套件.參考名稱)
所以去除套件部份之後就是我們想要的部份了
做成helper函數的原因是之後更新state時也需要這個函數,DRY,很好

接著我們看Dispatch的實作
func (s *Store) Dispatch(act *Action) {
    s.mu.Lock()
    if s.atSubscribe {
        panic(`you're trying to invoke Dispatch inside the subscribed function`)
    }
    for _, r := range s.reducers {
        funcName := getReducerName(r)
        s.GetState[funcName] = r(s.GetState[funcName], *act)
    }
    // we call subscribed function after state updated.
    s.atSubscribe = true
    for _, subscribtor := range s.subscribes {
        subscribtor()
    }
    s.atSubscribe = false
    s.mu.Unlock()
}
可以看到if atSubscribe程式就會崩潰,這樣能夠阻止想做蠢事的正常呼叫(但是你阻止不了硬要用recover卻不處理這個問題的人)(目前這部份有bug,事實上我們會先遇上deadlock而不是panic,雖然結果正確但是失去錯誤訊息的提示)
我們對操作上鎖
所以程式能夠安全的在共時程式中執行,而狀態更新上鎖也算是正常的設計
然後我們執行那些subscribe進來的函數,注意註冊的函數不能有參數,因為我也不知道要傳什麼參數給你,那很不合理對吧
而最後Subscribe的實作索然無味,就只是將我們可愛的函式們放入註冊函數集合中
func (s *Store) Subscribe(subscribetor func()) {
    s.subscribes = append(s.subscribes, subscribetor)
}

最後總結是為了Go的一些特性我們得要做出取捨,例如原版中,只有一個reducer的情況只需要使用getState就能得到狀態,但是我統一使用函數名稱做存取,因為Go不允許多載函數,而動態參數列亦不太適合(我們得在參數數量超過1時panic,0時檢查reducer的數量,都很麻煩)

另外state型別為interface{}的部份,這並不會造成任何問題,因為state只在reducer中被使用,因此強制轉型並不會造成問題

謝謝觀看,歡迎提出改進意見

2017年11月12日 星期日

Redux in go

剛才利用一點時間寫了一個Go版本的Redux
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
但是也會帶來別的麻煩,當然這又是另一個故事了...