🚀

Jelly v0.9 をリリースしました

2022/12/14に公開

はじめに

この記事は PureScript アドベントカレンダー 14 日目の記事です。

PureScript アドベントカレンダー 2022 はこちら

https://qiita.com/advent-calendar/2022/purescript

PureScript の UI ライブラリ、 Jelly について、前回記事からしばらく経ったので現状と導入したもの / 知見をまとめます。

前回記事はこちら

https://zenn.dev/yukikurage/articles/367d844d79de20

ライブラリを分けた

に分けました。正直そんなにメリットを感じていないのと、開発がやりにくいので router 以外は戻して良いかなぁと思っています

ドキュメントサイト

見た目を良い感じにしました。
Core Concepts / Custom Monad の章まで書いています。それ以降はまだです。

https://jelly.yukikurage.net/

要素毎に専用の el 関数を作った

el "div" [] do ...

div [] do ...

と書けるようにしました。なお定義している場所はここです

https://github.com/yukikurage/purescript-jelly/blob/master/src/Jelly/Element.purs

とても面倒くさかった

ルーターの仕様を単純化

単純な構成にして、既存のルーティングツールと統合しやすくしました。
Router を導入すると以下の 3 つの関数が使えるようになります。

pushState :: String -> Effect Unit
replaceState :: String -> Effect Unit
currentRoute :: Signal String

単純ですね! purescript-routing-duplex との組み合わせがよさそうです。

https://pursuit.purescript.org/packages/purescript-routing-duplex/0.7.0

SSG 機能を消し飛ばした

render 関数があるので

https://github.com/yukikurage/purescript-jelly/blob/9d1629adb31b93d583242b9a79f7e77fae6317f3/src/Jelly/Render.purs#L80

render :: forall m. MonadRec m => MonadHooks m => Component m -> m (Signal String)

これを使って SSG してください、ということにしました。実際ドキュメントサイトもこれを使っています。

Hooks について

Hooks はこんな感じで使えます。

component = hooks do
  -- ここに Hooks
  pure do
    -- ここにコンポーネント

Hooks Everywhere...?

イベントハンドラを Effect から Hooks に変更しました。

component = do
  button [ on click \event -> {- ここで Hooks が使える -} ] do
    text "Click me"

これによって、ハンドラ内で後述するカスタムモナドが使えるようになりました。

また、アプリケーション全体のエントリポイントも Hooks で書いて、runHooks で実行するようになっています。カスタム Hooks が何処でも使えるのは大きいのではないでしょうか。

カスタムモナド

大きな変更点として、Hooks にカスタムモナドを使えるようになりました。具体的には MonadHooks クラスが実装された型は Hooks として使えるようになります。

https://pursuit.purescript.org/packages/purescript-jelly-hooks/0.2.1/docs/Jelly.Hooks#t:MonadHooks

class MonadEffect m <= MonadHooks m where
  -- | Add a cleaner
  useCleaner :: Effect Unit -> m Unit
  -- | Unwrap a Signal
  useHooks :: forall a. Signal (m a) -> m (Signal a)


instance MonadHooks m => MonadHooks (ReaderT r m) where
  useCleaner = lift <<< useCleaner
  useHooks sig = do
    r <- ask
    lift $ useHooks $ flip runReaderT r <$> sig

ReaderT を使えば derivingMonadHooks にできるので、大域的な状態を持つ Hooks が作れます。

実際にルーターやドキュメントサイトで使用しています。

https://github.com/yukikurage/jelly-docs/blob/02c234498ed567574ee2e9eb5ee40219493c5242/src/JellyDocs/AppM.purs#L30

newtype AppM a = AppM (ReaderT AffjaxDriver (RouterT Hooks) a)

derive newtype instance MonadHooks AppM

大域的な状態として AffjaxDriver を持っていますが、これはドキュメントサイトが Static Site を生成するときと、ブラウザ側で動くときに、異なる AffjaxDriver を使う必要があるからです。

まとめ

短い記事になってしまいましたが、カスタムモナドを使えるようにしたのは大きいです。これによって自由度がかなり上がったのではないでしょうか。今後の予定は以下の通りです

  • 配列の効率的な表示
    • 現状、配列の並べ替えなどの操作に対しては配列全てを描画しなおすしか表示方法がありません。React のように、配列の並べ替え / 要素の追加を効率的に行えるようにします
  • O(n) での DOM 更新
    • 現在、要素の追加 / 削除時 "子要素の数 n に対して" O(n^2) の更新時間がかかっています (なお、テキストの更新などはテキストの長さを無視すれば O(1) です)。React などの差分検出 O(n) は "全ての要素の数 n に対して" なので単純比較はできませんが、子要素の数が増えても対応できるように、O(n) にするべきです。

これらを追加し、ベンチマークなどをして React などとの性能比較をしてから、正式版 1.0.0 をリリースしようと思います。

GitHubで編集を提案

Discussion