LoginSignup
10
6

自称モダンなLodash:Radashを触ってみた

Last updated at Posted at 2024-05-17

GitHub

何をするライブラリ?

underscore, lodashなどと同じ、ユーティリティ関数を集めたライブラリ。
公式曰く以下のコンセプト

  • Zero dependencies
  • Readable
  • Semi-Functional
  • Types

読み方

「ラダッシュ」(/raw-dash/)
「La」mbda - 「dash」 からきているとどこかのドキュメントに書いてあった記憶

触ってみた上での所感

公式のコンセプトを軸に評価。

Readable

→そのとおり:grin:

ライブラリの細かい仕様を確認したいってときにどうするかというとソースコードを確認するということがありますが、
ライブラリが大規模あるいは複雑だったりすると該当箇所を確認するためにもそれなりのノウハウと時間が必要です。

Radashの場合はライブラリのセクションがそのままソースコードの階層に対応しているのですぐ確認できます。
なのでライブラリをブラックボックスとして扱わなくていいし、必要なら使いたい関数だけコピペで持ってきてもいいです。

スクリーンショット 2024-05-12 14.11.10.png

スクリーンショット 2024-05-12 14.17.01.png

テストを見て仕様書がわりに確認することも苦にならないです。

Semi-Functional

→いまいち:thinking:

公式曰く、関数型プログラミングは便利なデザインパターンが多いから使いたくなるけど、みんながみんな「関数型プログラマー」じゃないよね、モナドとか知らなくても使えるよ!とのこと。
確かにガッツリ関数型で書くための開発標準を作ったりライブラリにどっぷり浸るのはハードル高いよなあ…と思うことはあるので、関数型プログラミングのいいところだけつまみ食いできるならしたいところです。

なのだが、List型とかResult型とかいわゆる「文脈を持った型」を取り扱うのに適した関数がないっぽく、個人的にはもうちょっと色々できると思っていたので期待値には届かなかった。
そういうことをしたければneverthrowとかts-patternとか、他のライブラリを使うほうがよさそう。

Zero dependencies, Types

→そのとおり:grin:

各セクションの良さげな関数

String

文字列の加工。

camel, snake, pascal, dash

camel, snake, pascal, dash
import { dash, camel, pascal, snake } from 'radash'

console.log(dash('green fish-blue_fish')) // => green-fish-blue-fish
console.log(camel('green fish-blue_fish')) // => greenFishBlueFish
console.log(pascal('green fish-blue_fish')) // => GreenFishBlueFish
console.log(snake('green fish-blue_fish')) // => green_fish_blue_fish

たまにこういう変換必要になる

Object

Objectに対する変換処理。

pick, omit

pick, omit
import { pick, omit } from 'radash'

const fish = {
  name: 'Bass',
  weight: 8,
  source: 'lake',
  barckish: false
}

pick(fish, ['name', 'source'])
// { name, source }

omit(fish, ['name', 'source']) 
// { weight, brackish }

オブジェクトのプロパティ削除は、deleteを使ったり分割代入をすることで実装できるがやや読みづらい実装になる。
pickやomitを使うと可読性高く目的を達成できる。

shake

shake
import { shake } from 'radash'

const ra = {
    one: 'one',
    two: false,
    three: undefined,
    four: null
}

console.log(shake(ra)) 
// { one: 'one', two: false, four: null }

console.log(shake(ra, a => !a)) 
// { one: 'one' }

2つめの引数に渡すと、条件に合致しないプロパティを削除できる。
omitの、削除するプロパティの条件を指定できるver
配列ではfilterとか使えばいいが、オブジェクトのプロパティに対して「フィルター」できるのがミソ

construct

フォームを動的に生成して、親子関係や配列を持ったデータを受け取りたいことってありますよね。

動的に生成されたフォームの例
  <div>
    <form action={handle}>
      <input type="text" name="name" id="name"/>
      <input type="text" name="power" id="power"/>
      <input type="text" name="friend.name" id="friend.name"/>
      <input type="text" name="friend.power" id="friend.power"/>
      <input type="text" name="enemies.0.name" id="enemies.0.name"/>
      <input type="text" name="enemies.0.power" id="enemies.0.power"/>
      <input type="text" name="enemies.1.name" id="enemies.1.name"/>
      <input type="text" name="enemies.1.power" id="enemies.1.power"/>
      <button type="submit">Submit</button>
    </form>
  </div>

こんな感じのformを作ってformDataを送ると

formData
  { name: 'name', value: 'ra' },
  { name: 'power', value: '100' },
  { name: 'friend.name', value: 'loki' },
  { name: 'friend.power', value: '80' },
  { name: 'enemies.0.name', value: 'hunter' },
  { name: 'enemies.0.power', value: '20' }

こんな感じののっぺりしたオブジェクトがやってきます。
これをパースしていい感じのオブジェクトにするために、constructが使えます。

  const rawData = Object.fromEntries(formData.entries())
  const data = construct(rawData)

とすると、

結果
{
  name: 'ra',
  power: '100',
  friend: { name: 'loki', power: '80' },
  enemies: [ { name: 'hunter', power: '20' }, { name: 'fff', power: '30' } ]
}

と、いい感じにパースして構造を持ったオブジェクトにできました。
constructはプロパティのpathを解析して、構造化してくれる関数というわけです。

また、以下のように

動的に生成されたフォームの例
  <form action={submitAction}>
    <input type="text" name="user[name]" />
    <input type="text" name="user[articles][0][title]" />
    <input type="text" name="user[articles][1][title]" />
    <button type="submit">Submit</button>
  </form>

こんな感じにブラケットを使ったpath表記でもうまいことパースしてくれてました。

結果
{
  user: { 
    name: 'aaa', 
    articles: [ 
      { title: 'bbb' }, 
      { title: 'ccc' } 
    ] 
  }
}

constructから逆算してinputのnameとかを決めるとよさそうです。

Array

Arrayに対する変換処理。

alphabetical, sort, counting, unique, select, sum

配列に対するシンプルな集計処理ならできる。
複雑なことをやりたかったら別のライブラリを使う方がよさそう。
selectはよくやる配列へのfilter+map

select, pick
// select
import { select, pick } from 'radash'

const fish = [
  {
    name: 'Marlin',
    weight: 105,
    source: 'ocean'
  },
  {
    name: 'Bass',
    weight: 8,
    source: 'lake'
  },
  {
    name: 'Trout',
    weight: 13,
    source: 'lake'
  }
]

console.log(select(
  fish,
  f => f.weight,
  f => f.source === 'lake'
)) 
// => [8, 13]

// select + pick
console.log(select(
    fish,
    f => pick(f, ['name', 'weight']),
    f => f.source === 'lake'
))
// => [ { name: 'Bass', weight: 8 }, { name: 'Trout', weight: 13 } ]

最後の例はpick と組み合わせ。
例えば配列で返すAPIレスポンスをクライアント側でフィルターして必要な項目だけ画面表示に使う、みたいな変換処理が楽に書けそう

cluster

cluster
import { cluster } from 'radash'
import { eachDayOfInterval, format } from 'date-fns'
import { ja } from 'date-fns/locale'

const days = eachDayOfInterval({ start: new Date(2024, 4, 1), end: new Date(2024, 5, 1)})
    .map((date) => format(date, 'yyyy/MM/dd', { locale: ja }))

console.log(cluster(days, 7)) // 2024/5/1から2024/6/1までの日付を7日ごとにグループ化

カレンダー表示とかページングなどで使えそう

first, last

first, last
import { first, last } from 'radash'

const gods = ['ra', 'loki', 'zeus']

console.log(first(gods)) // => 'ra'
console.log(first([], 'vishnu')) // => 'vishnu'
console.log(last(gods)) // => 'zeus'

配列の添字アクセスでもちろんできるが宣言的にしたいですよね。

objectify

objectify
import { objectify } from 'radash'

const people = [
    {
        name: 'Taro',
        x: 3,
        y: 4,
        z: 5
    },
    {
        name: 'Jiro',
        x: 6,
        y: 7,
        z: 8
    },
    {
        name: 'Saburo',
        x: 9,
        y: 10,
        z: 11
    }
]

console.log(objectify(
    people,
    f => f.name
)) 
// { Taro: { name: 'Taro', x: 3, y: 4, z: 5 }, Jiro: { name: 'Jiro', x: 6, y: 7, z: 8 }, Saburo: { name: 'Saburo', x: 9, y: 10, z: 11 } }

console.log(objectify(
    people,
    f => f.name,
    f => f.x * f.x + f.y * f.y + f.z * f.z
)) 
// { Taro: 50, Jiro: 149, Saburo: 302 }: Record<string, number>

配列をオブジェクトに変換でき、その際にキーとなるプロパティおよびバリューの算出方法を指定できる。
結果はRecordで返却される。
元の配列内の情報からいらない情報を削減してキーとバリューに圧縮したりできる。
実装はシンプル

Curry

関数型プログラミングっぽく書くためのライブラリ群
そこまで充実していないので、あまり使う機会ないかも

  • debounce, throttle

lodashにもあるが、debounce(処理を間引く)とthrottle(処理を一定時間ごとに実行)がある

debounce
import { debounce } from 'radash'

const makeSearchRequest = (event) => {
  api.movies.search(event.target.value)
}

input.addEventListener('change', debounce({ delay: 100 }, makeSearchRequest))

Async

非同期関数を扱うライブラリ群

  • tryit
tryit
import { tryit } from 'radash'

const findUser = tryit(api.users.find)

const [err, user] = await findUser(userId)

try/catchの代わりにtryitでラップすることで、実行結果を配列値で受け取ることができる。
同期的な関数も非同期関数もラップ可能。
Eitherとかで受け取れはせず、単なる配列で返ってくるのが個人的にはいまいち

  • parallel
parallel
// parallel
import { parallel, tryit } from 'radash'

const userIds = [1, 2, 3]

const [err , _] = await tryit(parallel)(3, userIds, async (userId) => {
  throw new Error(`No, I don\'t want to find user ${userId}`)
})

const err2 = err as AggregateError

console.log(err2.errors) 
//[Error, Error, Error]

console.log(err2.errors[1].message) 
// No, I don't want to find user 2

Promise.allなどのように並列に非同期処理を実行できる。Promise.allの場合非同期処理の中で一つでもエラーが発生すると全体としてエラーになるが、parallelの場合AggregateErrorとして結果を集約して受け取れる。
AggregateErrorとしてうまく推論できないので型アサーションせざるを得なかった(型ぢからが足りない…)

  • retry
retry, tryit
import { sleep, retry, partial } from "radash";

export async function findUser(error: boolean) {
    sleep(500)
    console.log('called')
    if(error) {
        throw new Error('error occured')
    }
    return 'asyncFunc';
}

const result = await retry({times: 2, delay: 500}, partial(findUser, true))
console.log(result)
// calledが2回出力された後、エラーが送出される

ラップするとAPI実行をリトライすることができる。
試行回数やエラー時の遅延秒数の指定、exponential backoff(リトライ時に段々delayを長くする)の指定も可能

  • defer

関数のスコープを抜ける時に必ず実行したい処理を書いておくと、処理がスコープを抜ける時にdefer文で登録した処理がフックされる。
JavaやJavaScriptでいうfinnalyブロックで行うような処理(典型的にはリソース解放)を入れることが多そう

defer
import { defer } from 'radash'

await defer(async (cleanup) => {
  const buildDir = await createBuildDir()

  cleanup(() => fs.unlink(buildDir)) // スコープを抜ける時にfs.unlinkが実行される

  await build()
})

ただ、TypeScript 5.2で導入されたusingキーワードに関連したDisposableStackのメソッドにdefer文がある。
つまりTypeScript標準ランタイムでdeferがサポートされたといっていいので使う機会はなさそう…
参考
参考2

DisposableStackとdeferの例
function doSomeWork() {
    const path = ".some_temp_file";
    const file = fs.openSync(path, "w+");
    using cleanup = new DisposableStack();  //←これ
    cleanup.defer(() => {
        fs.closeSync(file);
        fs.unlinkSync(path);
    });
    // use file...
    if (someCondition()) {
        // do some more work...
        return;
    }
    // ...
}

結論

  • オブジェクトや配列の変換については便利な関数があるので、こんな場面で使えそうだなーと想像しながら触ってみるとよい
  • 関数型プログラミングをするためのライブラリとして使うには物足りないので、他のライブラリを検討した方が良さそう
10
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
6