點燈坊

失くすものさえない今が強くなるチャンスよ

自行建立 RxJS 的 pick() Operator

Sam Xiao's Avatar 2020-07-15

很多 FP 語言與 FP Library 都有內建 pipe() Operator 只選取 Object 部分 Property,但 RxJS 只內建了 pluck(),但我們也可自行建立 pick() Operator。

Version

macOS Catalina 10.15.5
WebStorm 2020.1.2
Vue 2.6.11
RxJS 6.5.5
Ramda 0.27.0

Browser

pick000

按下 Get Book 僅顯示 titleprice

Data

{
  "id": 1,
  "title": "FP in JavaScript",
  "price": 100,
  "categoryId": 1,
  "image": "fpjs.jpg"
}

http://localhost:3000/books/:id 會回傳指定 id 的書籍。

可發現 API 回傳了眾多不需要的 property,顯示只需要 titleprice

map()

<template>
  <div>
    <button v-stream:click="{ subject: click$, data: 1 }">Get Book</button>
    <ul>
      <li v-for="(x, k) in book$" :key="k">{{ k }} : {{ x }}</li>
    </ul>
  </div>
</template>

<script>
import { ajax } from 'rxjs/ajax'
import { pluck, map, switchMap } from 'rxjs/operators'

let fetchBook$ = x => ajax(`http://localhost:3000/books/${x}`).pipe(
  pluck('response')
)

let subscriptions = function() {
  let book$ = this.click$.pipe(
    pluck('data'),
    switchMap(fetchBook$),
    map(x => ({ "title": x.title, "price": x.price }))
  )

  return { book$ }
}

export default {
  name:'App',
  domStreams: [
    'click$'
  ],
  subscriptions
}
</script>

19 行

let book$ = this.click$.pipe(
  pluck('data'),
  switchMap(fetchBook$),
  map(x => ({ "title": x.title, "price": x.price }))
)

可使用 map() 將所需 property 組合成 Object 回傳。

Ramda’s pick()

<template>
  <div>
    <button v-stream:click="{ subject: click$, data: 1 }">Get Book</button>
    <ul>
      <li v-for="(x, k) in book$" :key="k">{{ k }} : {{ x }}</li>
    </ul>
  </div>
</template>

<script>
import { ajax } from 'rxjs/ajax'
import { pluck, map, switchMap } from 'rxjs/operators'
import { pick } from 'ramda'

let fetchBook$ = x => ajax(`http://localhost:3000/books/${x}`).pipe(
  pluck('response')
)

let subscriptions = function() {
  let book$ = this.click$.pipe(
    pluck('data'),
    switchMap(fetchBook$),
    map(pick(['title', 'price'])),
  )

  return { book$ }
}

export default {
  name:'App',
  domStreams: [
    'click$'
  ],
  subscriptions
}
</script>

20 行

let book$ = this.click$.pipe(
  pluck('data'),
  switchMap(fetchBook$),
  map(pick(['title', 'price'])),
)

若要使 map() 的 callback 也 point-free,可使用 Ramda 的 pick(),將所需的 property 以 Array 傳入。

RxJS’s pick()

<template>
  <div>
    <button v-stream:click="{ subject: click$, data: 1 }">Get Book</button>
    <ul>
      <li v-for="(x, k) in book$" :key="k">{{ k }} : {{ x }}</li>
    </ul>
  </div>
</template>

<script>
import { ajax } from 'rxjs/ajax'
import { pipe } from 'rxjs'
import { pluck, map, switchMap } from 'rxjs/operators'
import { pick as _pick } from 'ramda'

let fetchBook$ = x => ajax(`http://localhost:3000/books/${x}`).pipe(
  pluck('response')
)

let pick = a => pipe(
  map(_pick(a))
)

let subscriptions = function() {
  let book$ = this.click$.pipe(
    pluck('data'),
    switchMap(fetchBook$),
    pick(['title', 'price'])
  )

  return { book$ }
}

export default {
  name:'App',
  domStreams: [
    'click$'
  ],
  subscriptions
}
</script>

20 行

let pick = a => pipe(
  map(_pick(a))
)

其實要成為能用在 RxJS pipe() 的 operator 也沒這麼難,只要是 unary function 且 其 signature 為傳入 Observable,回傳為 Observable 的 pure function 即可。

由於底層仍使用 Ramda 的 pick(),故先 alias 成 _pick(),至於 _pick() 所要的 Array,則以 currying 形式傳入。

25 行

let book$ = this.click$.pipe(
  pluck('data'),
  switchMap(fetchBook$),
  pick(['title', 'price'])
)

如此 pick() 則可直接使用在 pipe() 內,可將 pick() 寫在自己的 helper 或 library 內供日後重複使用。

Conclusion

  • 要成為 Function Pipeline 的 operator 並不難,Ramda 只要遵循 unary function 且為 pure function 即可;RxJS 也一樣,除了必須是 unary function 與 pure function 外,且 signature 還必須是 Observable
  • 若還要傳遞其他資料很難是 pure function 時,則可使用 curried function,只要讓 unary function 只接受 Observable 並回傳 Observable 即可