Algebraic Effects,以及它在React中的應用

Algebraic Effects是一個在編程語言研究領域新興的機制,雖然目前尚未工業語言實現它,可是在React社區會常常聽到關於它的討論。React最近的不少新特性的背後其實是Algebraic Effects的概念。所以,我花了一些時間來了解Algebraic Effects,但願體悟到React團隊是如何理解這些新特性的。html

Algebraic Effects

每個Algebraic Effect都是一次【程序控制權】的巡迴:react

【effect發起者】發起effect,並暫停執行(暫時交出程序控制權)
-> 沿着調用棧向上查找對應的effect handler(相似於try...catch的查找方式)
-> effect handler執行(得到程序控制權)
-> effect handler執行完畢,【effect發起者】繼續執行(歸還程序控制權)git

例子(這並非合法的JavaScript):github

function getName(user) {
  let name = user.name;
  if (name === null) {
      name = perform 'ask_name'; // perform an effect to get a default name!
  }
  return name;
}

function makeFriends(user1, user2) {
  user1.friendNames.add(getName(user2));
  user2.friendNames.add(getName(user1));
}

const arya = { name: null };
const gendry = { name: 'Gendry' };
try {
  makeFriends(arya, gendry);
} handle (effect) { // effect handler!
  if (effect === 'ask_name') {
    const defaultName = await getDefaultNameFromServer();
      resume with defaultName; // jump back to the effect issuer, and pass something back!
  }
}

console.log('done!');

注意幾點:編程

  1. effect發起者不須要知道effect是如何執行的(解耦),effect的執行邏輯由調用者來定義。redux

    這一點與try...catch相同,拋出錯誤的人不須要知道錯誤是如何被處理的。
    getName能夠當作純函數。易於測試。
  2. effect執行完之後,會回到effect發起處,並提供effect的執行結果。segmentfault

    這一點與try...catch不一樣,try...catch沒法恢復執行。
  3. 中間調用者對Algebraic Effects是無感的,好比例子中的makeFriends

Algebraic Effects 與 async / await 的區別

用async / await實現上面的例子:promise

async function getName(user) {
  let name = user.name;
  if (name === null) {
      name = await getDefaultNameFromServer();
  }
  return name;
}

async function makeFriends(user1, user2) {
  user1.friendNames.add(await getName(user2));
  user2.friendNames.add(await getName(user1));
}

const arya = { name: null };
const gendry = { name: 'Gendry' };

makeFriends(arya, gendry)
  .then(() => console.log('done!'));

異步性會感染全部上層調用者

能夠發現,makeFriends如今變成異步的了。這是由於異步性會感染全部上層調用者。若是要將某個同步函數改爲async函數,是很是困難的,由於它的全部上層調用者都須要修改。
而在前面Algebraic Effects的例子中,中間調用者makeFriends對Algebraic Effects是無感的。只要在某個上層調用者提供了effect handler就好。網絡

可複用性的區別

注意另外一點,getName直接耦合了反作用方法getDefaultNameFromServer。而在前面Algebraic Effects的例子中,反作用的執行邏輯是【在運行時】【經過調用關係】【動態地】決定的。這大大加強了getName的可複用性。架構

在async / await的例子中,經過依賴注入可以達到與Algebraic Effects相似的可複用性。若是getName經過依賴注入來獲得反作用方法getDefaultNameFromServer,那麼getName函數在可複用性上,確實與使用Algebraic Effects時相同。可是前面所說的【異步性會感染全部上層調用者】的問題依然存在,getNamemakeFriends都要變成異步的。

Algebraic Effects 與 Generator Functions 的區別

與async / await相似,Generator Function的調用者在調用Generator Function時也是有感的。Generator Function將程序控制權交給它的直接調用者,而且只能由直接調用者來恢復執行、提供結果值。

直接調用者也能夠選擇將程序控制權沿着執行棧繼續向上交。這樣的話,直接調用者(下面例子的makeFriends)本身也要變成Generator Function(被感染,與async / await相似),直到遇到能提供【結果值】的調用者(下面例子的main)。

function* getName(user) {
  let name = user.name;
  if (name === null) {
      name = yield 'ask_name';
  }
  return name;
}

function* makeFriends(user1, user2) {
  user1.friendNames.add(yield* getName(user2));
  user2.friendNames.add(yield* getName(user1));
}

async function main() {
  const arya = { name: null };
  const gendry = { name: 'Gendry' };
  
  let gen = makeFriends(arya, gendry);
  let state = gen.next();
  while(!state.done) {
      if (state.value === 'ask_name') {
          state = gen.next(await getDefaultNameFromServer());
      }
  }
}

main().then(()=>console.log('done!'));

能夠看出,在可複用性上,getName沒有直接耦合反作用方法getDefaultNameFromServer,而是讓某個上層調用者來完成反作用。這一點與使用Algebraic Effects時相同。
redux-sagas就使用Generator Functions,將反作用的執行從saga中抽離出來,saga只須要發起反作用。這使得saga成爲純函數,易於測試。

可是,依然存在感染調用者的問題。

React中的Algebraic Effects

React Fiber架構:可控的「調用棧」這篇文章中,咱們討論了React Fiber架構是一種可控的執行模型,每一個fiber執行完本身的工做之後就會將控制權交還給調度器,由調度器來決定何時執行下一個fiber。
雖然JavaScript不支持Algebraic Effects(事實上,支持Algebraic Effects的語言屈指可數),可是在React Fiber架構的幫助下,React能夠模擬一些很實用的Algebraic Effects。

Suspend

<Suspend>就是一個例子。當React在渲染的過程當中遇到還沒有就緒的數據時,可以暫停渲染。等到數據就緒的時候再繼續:

// cache相關的API來自React團隊正在開發的react-cache:
// https://github.com/facebook/react/tree/master/packages/react-cache
const cache = createCache();
const UserResource = createResource(fetchUser); // fetchUser is async

const User = (props) => {
    const user = UserResource.read( // synchronously!
        cache,
          props.id
    );
  return <h3>{user.name}</h3>;
}

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <User id={123} />
      </Suspense>
    </div>
  );
}
react-cache是React團隊正在開發的工具,將 <Suspense>用於數據獲取的場景,讓須要等待數據的組件「暫停」渲染。
目前已經上線的,經過 React.lazy來暫停渲染的能力,其實也是相似的原理。

理論上UserResource.read能夠看作發起了一個Algebraic Effect。User發出這個effect之後,控制權暫時交給了React(由於React是User的調用者)。React scheduler提供了對應的effect handler,檢查cache中是否有對應id的user:

- 若是在cache中,則當即將控制權交還給User,並提供對應的user數據。
- 若是不在cache中,則調用fetchUser從網絡請求對應id的user,在此過程當中,渲染暫停,`<Suspense>`渲染fallback視圖。獲得結果之後,將控制權交還給User,並提供對應的user數據。

實際上它是經過throw來模擬Algebraic Effect的。若是數據還沒有準備好,UserResource.read拋出一個特殊的promise。得益於React Fiber架構,調用棧並非React scheduler -> App -> User,而是:先React scheduler -> App而後React scheduler -> User。所以User組件拋出的錯誤會被React scheduler接住,React scheduler會將渲染「暫停」在User組件。這意味着,App組件的工做不會丟失。等到promise解析到數據之後,從User fiber開始從新渲染就行了(至關於控制權直接交還給User)。

若是直接使用調用棧來管理組件樹的渲染(遞歸渲染),那麼App組件的渲染工做會由於User拋出值而丟失,下次渲染須要從頭開始。

Hooks

React團隊將hooks都看作Algebraic Effect。useState的返回值取決於它的所處「虛擬調用棧」,即它在組件樹中的位置,即fiber。好比一個組件樹中有2個Counter組件,那麼這兩個Counter組件實例所處的上下文是不同的,所以它們的useState返回值是獨立的。

參考資料

Algebraic effects, Fibers, Coroutines...
Algebraic Effects for the Rest of Us

相關文章
相關標籤/搜索