iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 25
1
Software Development

Functional Programming in JS系列 第 25

Functor 5: 用 Effect functor 解決真實世界的 Side Effect

  • 分享至 

  • xImage
  •  

在第五天 Buzz word 2 : Side Effect 曾經說到會提供另一個很複雜解決 Side Effect 的方法: Effect functor,今天就要來兌現承諾了,也可以統整一下前幾天所學。這邊的 Effect 其實就是前幾天的 Box,你可以自己定義他的名字。
https://ithelp.ithome.com.tw/upload/images/20200925/20106426fzDh88BXJl.png
(↑ 嗨,我又出現了之 Box data type 圖)

const Effect = f => ({
  map: g => Effect( x => g(f(x)) ),
  runEffects: x => f(x) // 拿出來看看裡面是啥
});

Effect 這個 Functor 是把 function 當成參數,這很棒因為我們會想把導致 Side Effect 例如 Math.random()console.log 放到函式裡延後執行

以下模擬一個 web application,這個 application 需要根據不同瀏覽器跟使用者地區顯示不同使用者的資訊及簡介,為了方便起見這些資料被存進了 window config ,若今天我們想從 window.myAppConf get 相對應 select element 然後抓 DOM 裡面值

// 想抓這邊的值
window.myAppConf = {
    selectors: {
        'user-bio':     '.userbio',
        'article-list': '#articles',
        'user-name':    '.userfullname',
    },
    templates: {
        'greet':  'Pleased to meet you, {name}',
        'notify': 'You have {n} alerts',
    }
};
// html
<div class="userbio">Functor is hard</div>

以前寫法

一行結束非常簡潔

document.querySelector(window.myAppConf['user-bio']).innerHTML

但幾乎全部都是 Side Effect,這邊可以列出下列三個 Side Effect

  • window.myAppConf['user-bio']): 抓 function scope 外的值
  • document.querySelector: 操作 DOM
  • innerHTML: 讀取 DOM 裡的值

以下就要根據這三個 Side Effect 使用 Functor 寫法做改進

Effect Functor

我們可以寫一個 Effect data type 並給予 of 方法,這樣他回傳的就會是 Effect 這個 data type 而不是直接傳 value 丟 Side Effect 出來

// of :: a -> Effect a
Effect.of = val => Effect(() => val);

Side Effect 1: window.myAppConf['user-bio'])

const win = Effect.of(window); // return 另一個 Effect
const userBioLocator = win.map(x => x.myAppConf.selectors['user-bio']);
// Effect('.userbio')

Side Effect 2: document.querySelector

再來想要用 document.querySelector() 去抓 DOM ,恩另一個 Side Effect,所以一樣用 Effect.of 把他包成 Pure 的,這樣回傳也會是一個 Effect

// $ :: String -> Effect DOMElement
function $(selector) {
    return Effect.of(document.querySelector(selector));
}

若想要結合以上兩個,可以用 map()

const userBio = userBioLocator.map($);
// Effect(Effect(<div>))

Side Effect 3: innerHTML

好啦現在終於可以抓 DOM 裡面值了

const innerHTML = userBio.map(eff => eff.map(domEl => domEl.innerHTML));
// Effect(Effect('<h2>User Biography</h2>'))

完整程式碼

連再一起寫會是

const Effect = f => ({
  map: g => Box( x => g(f(x)) ),
  runEffects: x => f(x) // 拿出來看看裡面是啥
});

Effect.of = val => Effect(() => val);

function $(selector) {
    return Effect.of(document.querySelector(selector));
}

Effect
  .of(window)
  .map(x => x.myAppConf.selectors['user-bio']);
  .map($)
  .map(eff => eff.map(domEl => domEl.innerHTML))

// Effect(Effect('<h2>User Biography</h2>'))

https://ithelp.ithome.com.tw/upload/images/20200925/20106426S1r6vJlpKt.jpg
崩潰狀,已經出現巢狀 Effect 了,明明以前一行結束的東西,現在為了避免 SIde Effect 變如此複雜,其實是可以寫更好的

Refactor 1 之 flatMap

https://ithelp.ithome.com.tw/upload/images/20200925/20106426nn2W5krmqB.jpg

// Effect :: Function -> Effect
const Effect = f => ({
    map: g => Effect( x => g(f(x)) ),
    runEffects: x =>  f(x),
    flatMap: x =>  f(x)
}
const userBioHTML = Effect.of(window)
    .map(x => x.myAppConf.selectors['user-bio'])  // Effect('.userbio')
    .map($) // // Effect(Effect(<div>))
    .flatMap()  // Effect(<div>)
    .map(x => x.innerHTML); // Effect('<h2>User Biography</h2>')
    .runEffects() // Functor IS HARD

藉由 flatMap 可以扁平化,讓寫起來更精簡一點

Refactor 2 之 chain

https://ithelp.ithome.com.tw/upload/images/20200925/2010642603fAoi9frO.jpg
Chain 就是 map + flatMap

// Effect :: Function -> Effect
const Effect = f => ({
    map: g => Effect( x => g(f(x)) ),
    runEffects: x =>  f(x),
    flatMap: x =>  f(x),
    chain: g =>  Effect(f).map(g).flatMap()
}

可以變成更精簡

const userBioHTML = Effect.of(window)
    .map(x => x.myAppConf.selectors['user-bio'])
    .chain($)
    .map(x => x.innerHTML);
// ← Effect('<h2>User Biography</h2>')

完整程式碼 codepen


參考文章

如有錯誤或需要改進的地方,拜託跟我說。
我會以最快速度修改,感謝您

歡迎追蹤我的部落格,除了技術文也會分享一些在矽谷工作的甘苦。


上一篇
Functor 4: 圖解 Box Data Type 之方法 map、flatMap、chain
下一篇
[練習] Functor Exercise
系列文
Functional Programming in JS30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

1
Alex Liu
iT邦新手 5 級 ‧ 2020-10-19 14:04:15

無意間發現有些地方有漏掉一些字,像是

// Effect :: Function -> Effect
const Effect = f => ({
    map: g => Effect( x => g(f(x)) ),
    unEffects: x =>  f(x),
    flatMap: x =>  f(x)
}

這裡應該是

// Effect :: Function -> Effect
const Effect = f => ({
    map: g => Effect( x => g(f(x)) ),
    runEffects: x =>  f(x),
    flatMap: x =>  f(x)
}

在下面的有漏掉一樣的地方

// Effect :: Function -> Effect
const Effect = f => ({
    map: g => Effect( x => g(f(x)) ),
    unEffects: x =>  f(x),
    flatMap: x =>  f(x),
    chain: g =>  Effect(f).map(g).flatMap()
}
hannahpun iT邦新手 4 級 ‧ 2020-10-21 14:42:11 檢舉

感謝糾正,已經改囉

0
Raymond
iT邦新手 4 級 ‧ 2021-02-19 23:04:27

Functor 好難,看範例就頭昏眼花了,還真不知道怎麼導入專案XD、妳真厲害!
但有發現一個奇怪的地方:

const Effect = f => ({
  map: g => Box( x => g(f(x)) ), // <- 這裡應該是要回傳 Effect Type 才對
  runEffects: x => f(x)
});

Effect.of = val => Effect(() => val);

function $(selector) {
    return Effect.of(document.querySelector(selector));
}

Effect
  .of(window)
  .map(x => x.myAppConf.selectors['user-bio']);
  .map($)
  .map(eff => eff.map(domEl => domEl.innerHTML))

// Effect(Effect('<h2>User Biography</h2>'))

我要留言

立即登入留言