purescript-pure-styleをつくりなおした

追記

本記事に出てくるパッケージは現在メンテされておりません。 ご注意ください。

概要

purescript-pure-styleのバグ修正したり、全面的に新しくしました。 何をするパッケージかというとCSSをPureScriptで書き、クラス名などを自動生成するパッケージです。 変更の概要としては以下の通りです。

  1. ディレクトリ構造の自由度をあげるための変更を行いました
  2. より最低限のことしかしないようにしました
  3. 自動生成時にクラス名の先頭文字列が数字になる問題の修正

ディレクトリ構造の自由度をあげるための変更について

以前は、以下のように、与えたスタイル配列から、自動生成された命名とスタイル文字列をもったデータをつくる、というつくりでした。

type Result =
  { name :: String -- クラス名など
  , output :: String  -- スタイル文字列
  }

className :: Array Style -> Result -- 関数の処理定義は省略

 sampleClass = className
    [ "width" : "100px"
    , "height" : "100px"
    , "&:hover" :-
        [ "width" : "200px"
        , "height" : "200px"
        , "&:disabled" :- [ "color" : "red" ]
        , ".selected" :- [ "color" : "blue" ]
        ]
    ]

よって、各種スタイル定義のスタイル文字列を連結して、1枚のcss文字列をつくり、それをstyleタグに吐くというようなことが必要でした。 つまり、どういうことかというと、少なくとも以下のようなファイルが必要でした。

import Style.Hoge (hogeContainer, link, button)
import Style.Fuga (fugaContainer)
-- 以下同文...というようなスタイル定義モジュールからスタイルを全部引っ張って

styles = fold 
  [ hogeContainer
  , link
  , button
  , fugaContainer
  -- ...以下同文
  ]

この場合、importしやすいようにスタイル定義はスタイル用のディレクトリに集めて、viewとは離れたディレクトリにスタイル定義を並べるという構造でないとかなり面倒になります。

一方で、スタイル(共通スタイル以外のだよ)はviewの近くに定義したほうが、cssで本当によくある、消し忘れ、修正忘れがかなり減るし、スタイルの影響範囲が一目瞭然なので、ここ3年くらいはそのようにしています。

例えばReactであれば、以下のようにしてファイルをつくります。

/UserList
  /index.js (viewの本体)
  /style.js (あるいは、style.cssなど)

さらには、あるコンポーネントでしか使わないコンポーネントは以下のようにやったりします。これも影響範囲が一目瞭然だからです。

(わかりやすくするために、view本体とstyleについてしか論じてないですよ)
/UserList
  /index.js (viewの本体)
  /style.js (あるいは、style.cssなど)
  /UserItem
    /index.js
    /style.js

実際にこれをみんながよしとするかどうかは別として、選択肢として存在することは確かかと思います。

これを、上記のpure-styleでやろうとすると、import文はかなり厳しい感じなります。 スタイルを結合するために、大変なimport文を書くことになるでしょう。

では、どうしたかというと、RefをもったStyleSheetという型を経由して、スタイル生成 + Refへの反映という形にすることによって、これを回避することにしました。

newtype StyleSheet = StyleSheet (Ref String)

createStyleSheet :: StyleSheet

getStyle :: StyleSheet -> String

registerStyle :: StyleSheet -> String -> String

参照を保持し続けて、スタイル定義するたびに、その参照にスタイル定義を足していき、最終的にcreateStyleSheetつくったStyleSheetが定義してあるモジュールだけをimportして、getStyleするだけで済みます。 これは、内部的にはunsafeな処理を使い、Effがからんでないのに、参照保持をしているという感じになっています。 ただ、これによって、使い手にディレクトリ構造の自由度を与えました。

より最低限のことしかしないようにしたことについて

これは、今回で一番大胆な変更でした。 元々は内部の型付けがあまりよくなくて改善し始めたのがきっかけだったのでした。 型付けを改善し始めたものの、そもそもPureScriptでcss書くにしても、他の関数型のcssライブラリのように執拗な型付けをするのはかなり大変なのでそれはしたくないし、少なくとも僕はそれを求めていませんでした。 そういう背景で中途半端に型付けしたところで、安全なスタイル定義は実現できない、と考えました。 もともと僕は、自動命名とJS(ここではPS)とスタイル間の値共有さえできれば、あとはいらないと思っていたので、1mmも型付けを頑張らない方向に倒してAPIを極限まで減らすという方向に倒すことにしました。端的に言えばcssの型付けなんて1mmも頑張りたくないという考え方に振り切りました。 具体的には、記法が大幅に変わりました。以下が例です。

-- 以前
 sampleClass = className
    [ "width" : "100px"
    , "height" : "100px"
    , "&:hover" :-
        [ "width" : "200px"
        , "height" : "200px"
        , "&:disabled" :- [ "color" : "red" ]
        , ".selected" :- [ "color" : "blue" ]
        ]
    ]

-- 刷新後
sampleClass = registerStyle sheet
  """
  .& {
    width: 100px;
    height: 100px;
  }
  .&:hover {
    width: 100px;
    height: 100px;
  }
  .&:hover .selected {
    color: blue;
  }
  """

このように、スタイル部分は完全な文字列にしました。渡した文字列でhashをつくり、スタイル文字列内の&をそのhashで置き換えることで、スコープを作り出す以外のことはなにもしないようにしました。 結構見た目が異形感あるのですが、styled-componentsは似たような文字列ベースのスタイル定義なのにもかかわらずユーザーがぐっと増えてきていて、実際に文字列であるが故のいいところもあったりして(例えば、devtoolからそのままコピペできます)、この記法はなしじゃないのだろうと判断して、このようにしました。

自動生成時にクラス名の先頭文字列が数字になる問題の修正

これは単純なバグで、自動生成したクラス名の先頭が数字になることがあったが、今のcssは先頭数字ダメなので、hashにprefixをつける対応を入れました。

前と比べていまいちになったところ

ネスト記法は消えました

以前は、上記のように、スタイルのネスト記法が使えましたが、新しい記法のスタイル文字列は、現状、完全にプレーンなcssなのでネスト記法はできません。 ただ、これについては、scss -> radium -> css-modules -> free-styleと実務で使ってきて、scss以降は、そもそも名前被りを気にしないので、ネストすることが激減したという経験に基づき、そのような環境下では深いネストになること自体間違っていると思う前提だと、浅いネストをプレーンなcssで書くのは全然苦ではないので、私はネスト記法は対応しませんでした。

埋め込み文字列がない故に少し記法として不便になる

JavaScriptの場合、埋め込み文字列が使えるので、見栄えはそんなに悪くないが、PureScriptの場合、それはないので、Semigroup<>で結合するような形になり、見栄えはあまりよろしくないと思うかもしれない。