PureScriptの孤児

はじめに

PureScript Advent Calendar 2017 - Qiitaの6日目の記事です。

小ネタです。

Orphan instanceって何よ

以下のコードをみてください。

instance showUnit :: Show Unit where
  show _ = ""

ビルドしてみます。

  Type class instance showUnit for
                       
    Data.Show.Show Unit
                       
  is an orphan instance.
  An orphan instance is one which is defined in a module that is unrelated to either the class or the collection of data types that the instance is defined for.
  Consider moving the instance, if possible, or using a newtype wrapper.

これはOrphan instanceだ、と怒られていますね。

これは何かというと、インスタンス定義したモジュールが、型クラスを定義したモジュールでもなければ、インスタンスにしたいデータ型を定義したモジュールでもない場合のインスタンスを指しているようです。

Orphan instanceはPureScriptでは禁止されていて、コンパイルに失敗します。

確かにこの制約がない場合、どこで何のインスタンスが定義されているのかわけわからなくなりそうですね。定義が重複したりもしそうです。これを人間の脳で気をつけるのは大変そうです。

パッケージ提供者が型クラスを提供する場合は、必要なプリミティブ、また、その型クラスにおいてよく使用されるとみなせる型について、インスタンス定義をしておくと親切ということなのだろう。

こんな風に怒られたらどうするの?

エラーメッセージに書いてある通りですね。 「おまえはこういう理由でまちがっている」だけじゃなくて「こうしてくれ」まで書かれていました。

Consider moving the instance, if possible, or using a newtype wrapper

まとめ

人間同士の指摘も、「おまえはまちがっている」だけじゃなくて「おまえはこういう理由でまちがっているのでこうしてくれ」まで言えるとコミュニケーションコスト減ると思います。

PureScriptで意識の低いSPA用パッケージをつくりました

追記

本記事のパッケージは現在メンテされておりません。 代わりのUIパッケージとして書き直したこちらを是非チェックしてみてください。 purescript-freedom

はじめに

PureScript Advent Calendar 2017 - Qiitaの3日目の記事です。 今年につくった、意識の低いSPA用パッケージであるGitHub - oreshinya/purescript-cherry: No longer maintenanceを紹介させていただきます。

使い方

カウンターを例に書いていきます。

Stateを定義しましょう

type State =
  { count :: Int
  }

initialState :: State
initialState =
  { count: 0
  }

Stateはアプリケーションが抱える状態です。昨今ではおなじみのSingle sourceです。アプリケーション全体の状態を管理します。 カウンターなので、countという状態を持たせることにしてみます。

Storeをつくりましょう

import Prelude
import Cherry.Store as S
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Ref (REF)

store :: forall e. S.Store (ref :: REF | e) State
store = S.createStore initialState

select :: forall e a. (State -> a) -> Eff (ref :: REF | e) a
select = S.select store

reduce :: forall e. (State -> State) -> Eff (ref :: REF | e) Unit
reduce = S.reduce store

3つの関数を定義しました。ひとつはstoreです。さきほど定義したinitialStateを使って初期化しています。 selectreduceは両方ともアプリケーション内でStoreから状態を取得、あるいは、状態を変更するために必要な関数となります。 それぞれ、State -> aState -> Stateといった型の関数を渡してもらうことになります。 それぞれSelectorReducerと呼ぶことにします。

Routerをつくりましょう

import Prelude
import Cherry.Router (router)
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Ref (REF)
import Data.Maybe (fromMaybe)
import DOM (DOM)
import Rout (match, end)

data Route
  = Home
  | NotFound

type State =
  { count :: Int
  , route :: Route
  }

initialState :: State
initialState =
  { count: 0
  , route: Home
  }

detectRoute :: String -> Route
detectRoute url = fromMaybe NotFound $ match url $ Home <$ end

route :: forall e. Eff (dom :: DOM, ref :: REF | e) Unit
route = router change
  where
    change url = reduce $ _ { route = detectRoute url }

まずRoute型を定義します。ホーム画面と404画面があるという想定で書いています。

そして、ルートの情報を管理しないといけなくなったので、さきほど定義したStateinitialStateに修正を入れています。

detectRouteは、urlからRoute型をつくります。実装については、purescript-routを使って行なっています。 ここでは細かい使い方は割愛します。

routeルーターの宣言です。change関数は先ほど定義したreduceを通してStaterouteを更新します。 このルーターは後々アプリケーションの初期化時に使います。

Viewをつくりましょう

import Cherry.Router as R
import VOM (VNode, h, t, (:=), (~>))

home :: forall e. Int -> VNode (dom :: DOM, ref :: REF, history :: HISTORY | e)
home count =
  h "div" []
    [ h "h1" [] [ t "Home" ]
    , h "div" [] [ t $ show count ]
    , h "a" [ "onClick" ~> (const $ R.navigateTo "/not_found") ] [ t "404 Not Found" ]
    ]

notFound :: forall e. VNode e
notFound =
  h "div" []
    [ h "h1" [] [ t "404" ]
    , h "a" [ "href" := "https://github.com/oreshinya/purescript-cherry", "target" := "_blank" ] [ t "Github" ]
    ]

view :: forall e. State -> VNode (dom :: DOM, ref :: REF, history :: HISTORY | e)
view state =
  case state.route of
    Home -> home state.count
    NotFound -> notFound

ホーム画面と404画面を定義しています。最後のview関数でrouteによって、画面を切り替えています。このview関数は後々アプリケーションの初期化時に使います。 コード中に出てくるVNode型は仮想DOMの型で、purescript-vomによって提供されています。 ここでは細かい使い方は割愛します。

Reducerをつくりましょう

incr :: State -> State
incr s = s { count = s.count + 1 }

カウントを増やせるようにするために、incr関数をつくりました。

Actionをつくりましょう

increment :: forall e. Eff (ref :: REF | e) Unit
increment = reduce incr



home :: forall e. Int -> VNode (dom :: DOM, ref :: REF, history :: HISTORY | e)
home count =
  h "div" []
    [ h "h1" [] [ t "Home" ]
    , h "div" [ "onClick" ~> const increment ] [ t $ show count ]
    , h "a" [ "onClick" ~> (const $ R.navigateTo "/not_found") ] [ t "404 Not Found" ]
    ]

便宜上、Action命名していますが、呼び名はなんでもよいです。 このレイヤーは、ユーザー操作から実際の状態変更の間を請け負うレイヤーです。 ここでは、もっとも簡単な例として、さきほどのincrreduceに渡すだけのincrementを作りました。 実際には、このレイヤーでは、affjaxなどをつかって通信を送り、結果をまってからreduceするなど、 いくらかの副作用を伴った処理になるでしょう。今回は使用しませんでしたが、上述したselect関数もここで使われるような想定です。

つくったActionをホーム画面で使います。 これでカウントアップできるようになりました。

初期化しましょう

import Cherry (mount)
import Cherry.Renderer (createRenderer)

main :: Eff (dom :: DOM, ref :: REF, history :: HISTORY, console :: CONSOLE) Unit
main = do
  renderer <- createRenderer "#app" view
  mount store renderer [ route ]

これまでにつくったviewを用いて、rendererをつくり、storerouteと合わせて、アプリケーションをマウントします。

mountの最後の引数は、マウント時に一度だけ発火するEffの配列を渡します。

reduceを通して状態を変更すると、viewが差分更新するようになります。

全体的にはこんな感じ

module Main where

import Prelude

import Cherry (mount)
import Cherry.Renderer (createRenderer)
import Cherry.Router as R
import Cherry.Store as S
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE)
import Control.Monad.Eff.Ref (REF)
import DOM (DOM)
import DOM.HTML.Types (HISTORY)
import Data.Maybe (fromMaybe)
import Rout (match, end)
import VOM (VNode, h, t, (:=), (~>))



data Route
  = Home
  | NotFound

type State =
  { count :: Int
  , route :: Route
  }



initialState :: State
initialState =
  { count: 0
  , route: Home
  }



store :: forall e. S.Store (ref :: REF | e) State
store = S.createStore initialState

select :: forall e a. (State -> a) -> Eff (ref :: REF | e) a
select = S.select store

reduce :: forall e. (State -> State) -> Eff (ref :: REF | e) Unit
reduce = S.reduce store



detectRoute :: String -> Route
detectRoute url = fromMaybe NotFound $ match url $ Home <$ end

route :: forall e. Eff (dom :: DOM, ref :: REF | e) Unit
route = R.router change
  where
    change url = reduce $ _ { route = detectRoute url }



home :: forall e. Int -> VNode (dom :: DOM, ref :: REF, history :: HISTORY | e)
home count =
  h "div" []
    [ h "h1" [] [ t "Home" ]
    , h "div" [ "onClick" ~> const increment ] [ t $ show count ]
    , h "a" [ "onClick" ~> (const $ R.navigateTo "/not_found") ] [ t "404 Not Found" ]
    ]

notFound :: forall e. VNode e
notFound =
  h "div" []
    [ h "h1" [] [ t "404" ]
    , h "a" [ "href" := "https://github.com/oreshinya/purescript-cherry", "target" := "_blank" ] [ t "Github" ]
    ]

view :: forall e. State -> VNode (dom :: DOM, ref :: REF, history :: HISTORY | e)
view state =
  case state.route of
    Home -> home state.count
    NotFound -> notFound



incr :: State -> State
incr s = s { count = s.count + 1 }



increment :: forall e. Eff (ref :: REF | e) Unit
increment = reduce incr



main :: Eff (dom :: DOM, ref :: REF, history :: HISTORY, console :: CONSOLE) Unit
main = do
  renderer <- createRenderer "#app" view
  mount store renderer [ route ]

リポジトリのサンプルにも似たようなサンプルコードをのせています。そちらのほうがもうちょっとサンプルとしてバリエーションがあります :)

なぜつくったか

もともとはpuxを使おうとしていたのですが、まずは以下をみてください。

data Event = Increment | Decrement

-- | Return a new state (and effects) from each event
foldp :: ∀ fx. Event -> State -> EffModel State Event fx
foldp Increment n = { state: n + 1, effects: [] }
foldp Decrement n = { state: n - 1, effects: [] }

-- | Return markup from the state
view :: State -> HTML Event
view count =
  div do
    button #! onClick (const Increment) $ text "Increment"
    span $ text (show count)
    button #! onClick (const Decrement) $ text "Decrement"

ある状態遷移をしたいときは、まずEventを定義し、foldpに足したEventについて行う処理を足していく、というような形になっていて、Eventとその処理に分割されています。(てかほぼElm Architectureなんですけど)

そして、実際のアプリケーションでは、これらの処理は縦にずらーっと並ぶような形になります。 あまり見目麗しくはないので、私はできればこれを避けたいと考えました。 もっと言えば、このEventとかいうものを定義したくないと思いました。

この構造は、実際にフレームワークをunsafeな処理をいれずにつくろうとしたら自然にこうなった記憶があるのでたぶん必然的な構造ではあると思うのですが、 一旦そういった言語とか実装の都合は置いといて、この構造について単なる構造的なメリットを考えると、time travel debugging時(puxにそういうのあるか知らないけど)に操作に名前がついていてみやすいということ、そしてもう一つは、理屈上は1つのイベントに対して複数の処理をひもづけられることということかと思います。

Eventをなくすとなると、

前者のメリットがなくなることについては、少しデメリットなのですが、ジェーエスでreduxをやっていたときの感想として、私はtime travel debuggingはたまにしかやらないうえ、イベント名をほとんどみないタイプだったのでそんなに困んなそうだなと思ったこと(ちなみにcherryはまだtime travel debuggingを提供していないです)、

後者のメリットがなくなることについても、まず滅多にそんなことやらなくて、わりと複雑めなSPAで基盤的処理にちょっとだけこの性質を利用したことがあるという程度なので、困らなそうだなと思ったこと、

から、Event排除の方向でいくことにしました。

JS界隈でreduxの流れから、ducksrepatchredux-zeroが出てきているのですが、たぶん発想の源流は似たようなもんなのではないかなと考えています。

そして、ゆとりプログラマらしい発想で、StoreにStateRefを持たせたものを内部的にunsafePerformEffでつくることによって、main関数の流れから外れているところでも同じstoreを参照できる、という構造にし、Event定義をせずにすむようにしました。

最後に

このフレームワークは、巧妙な型を使ってできているというわけではないので、PureScriptの基礎的なことを知っていれば、すぐに使い始めることができるかと思います。 でも、こんな何処の馬の骨かわからんようなもんがつくったものよりも、実績のあるフレームワークがあるので多分誰も使わないでしょう。 私の趣味で作ったので、自分で使い込んでいっていい感じにしていきたいと思います。(飽きる可能性もありますが)

どうやってPureScriptに慣れたのか

はじめに

PureScript Advent Calendar 2017 - Qiitaの1日目の記事が埋まっていなかったので大急ぎで書くことにしました。

元々はRubyJavaScriptなどの動的言語をメインでメシを食べているプログラミング的ゆとり世代であった私が、 少なくとも一般論としては学習が大変だと評されているPureScriptを、 (上手いか下手かは別として)普通に書けるようになった要因を記憶の限り辿ってみようと思います。

つまり、この記事は、「最初からPureScriptの勉強のためにやっていた」ということではなく、「たまたまそういう道を通ったけど、 あれがよかったのかもしれない」という回想です。

道のはじまり

この道のはじまりはHaskellでした。

特に仕事で使うわけでもなく、関数型言語に興味があったわけでもなく、ただなんとなく暇を持て余していたので暇つぶしとして思いついて勉強し始めました。どうやって勉強していたかというと、いわゆるすごいH本をダラダラ読みながら適宜写経するという方法でした。 この本は、基本的な構文や畳み込み、型クラス、IO、ファンクター、アプリカティブファンクター、いくつかの基本的なモナドなどについて、ゆとりでもわかりやすく書かれていました。どういうものなのか?なんのためにあるのか?ということがある程度はわかった気になれたと思います。

ちなみにこの時点では、基本的に知識の定着はしないですが、雰囲気だけは忘れずに頭に残っている状態になります。 本による勉強ってそんなもんだと思っているので、あまり真剣になりすぎず流し読みするのが良いでしょう。

また、アプリカティブファンクターについては、実用時の想像が頭になく、いったいなんなんだこれは?という感覚をもっていましたが、 Applicativeのススメ - あどけない話を読んだらそれは解消されました。 メリットやアプリカティブスタイルであるときとないときのコードの違いが書かれていて腑に落ちました。

モナドについては、モナドの力がいまいちわかりませんでしたが、

Stateモナドがわかればモナドがわかる - セカイノカタチモナドってなんだよ!?全然わからないんで分解して図解してみた(´・ω・`) - セカイノカタチを読んだことによって、理解が進んだように思います。

実際に書きはじめる

Spock

とりあえずフレームワーク使って書いてみようということで、フレームワークを調べました。 初心者の悲しい性ですが、シンプルで素朴な感じのフレームワークはどれだろうか?という視点で調べていてヒットしたのがSpockでした。

はじめたものの、SpockM conn sess st ()←この型を見ても意味不明で、とりあえず理解することを一旦諦めました。 当然意味がなにもわかっておらず、コンパイラに怒られまくっていたのですが、エラーの内容を見て直し続けると、 どういうわけか動いてくれるという不思議な体験をしました。 仕事で静的型付け言語を書いたこともあり、コンパイルエラーのときは常にいらついていた私でしたが、この時に「もしかしてコンパイラって友達なの?」という感覚が芽生えたのは新鮮でした。「俺たちは雰囲気でプログラミングをやっている」という感覚でした。 ここで身についたのは知識というよりも「コンパイラに身を委ねる感覚」だったかなと思います。

Servant

知り合いのHaskellerに「フレームワークは何がいいのか?」と聞いたら、servant – A Type-Level Web DSL — servant documentationだ、ということなので使い始めました。 「型でルーティングするとかまじかっけーな」という小学生並みの感想だったのですが、このフレームワークは私にとってはかなり勉強になりました。

これはドキュメントがお宝だらけだったからだと感じています。例えば、チュートリアルの最初に言語拡張の宣言が書いてあったりするのですが、なんとなく学習していたらお目にかかれないものが山ほど書かれていて、1個1個調べるのは大変でしたが、かなり勉強になりました。

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}

私はこのServantを土台に、認証、DB操作やプーリング、JSONのレスポンス生成などを試すことによって、上記の言語拡張に加えて、各種モナドトランスフォーマーや自動導出、自然変換など、色々なことを知ることができました。

当初は、とにかくコンパイル通すのに四苦八苦していましたが、 コンパイルエラーのメッセージが尋常じゃないくらい親切だった記憶があり、 コンパイルエラーと戦っているだけで訓練されていっている感覚がありました。 この時は、何かを調べようとすると何かを調べないといけない、みたいな感じになることも多く、一番勉強になった時期だったかと思います。 私はServantで普通のAPIサーバーを書くのはかなり効率のいい学習方法だったように感じています。

Elm

上記のようにHaskellを勉強したのち、フロントエンドも関数型で書いてみたかったのでElmを書き始めました。 Haskellを学習した後にやったためか、頭おかしいくらい学習コスト低いと感じてびっくりしたのを覚えています。

掲示板をつくりかけてみたり、自分のgithubページをElmで書き換えて見たり、Haskellとあわせて個人サービスをつくりかけてみたり、全部途中でやめる程度ではありますが、認証、通信、port、デコード、タイマー、各種イベント、それなりに色々できた感じがします。

Elmは型クラスないので、当然モナドもないのですが、書いていると「あ〜このへんがこうなってるのは副作用が伴う処理だからこんな感じになってるんだよな」みたいな感じのことを考えながら、フロントエンドという分野で、「何にどう型付けしているのか」が結構理解できました。

PureScript

ElmのあとにPureScriptを始めました。やっとPureScriptの話ができます。

と言いたいところですが、実はこのセクションについては書くことがほとんどありません。 もうすでにこの時点で、言語そのものが理由で書くのにつまるということがほとんどなくなっていました。(Effだけちょっとハマったけど)

一応Read PureScript by Example | Leanpubは読みましたが、ほぼ流し読みで、一生懸命読んだのはEffの章とFFIの章くらいです。

話は変わりますが、私はPureScriptをはじめてから型レベル計算を真面目に勉強したのですが、GitHub - paf31/purescript-quickserve: Quick HTTP serversが入門としてかなりわかりやすかったです。 各種エンドポイントでRecordをつくり、そのRecordでルーティングするところを読むと結構理解が進みます。

全体を通してやっていたこと

基本的に移動中は、使おうとしているパッケージを読むということをしていました。大体最初はほとんど読めないし、だるくなって読むのやめたりするのですが、そういうのはあまり気にしません。時間が解決してくれます。必要なのはコードの実態を目で見ることだけで、それさえしておけば、書いているうちに理解が後追いで一気に来ると思います。

最後に

急がば回れではないですが、HaskellとElmを先にやったのは結構よかったんじゃないかと思います。 Haskellは非常に情報量が多く調べやすいこと、そして、PureScriptよりも親切なエラーメッセージがあり、 調査に困らないし、コンパイラが勝手に訓練してくれます。ビシバシと鞭でケツを叩かれている感じで、ンギモヂイイイイイイイイイイイイイイイイイイという感じでしょうか。おそらく、PureScript書く上で、この時の体験が一番大きいです。

また、Elmに関しては、学習コストが低いため、ブラウザという戦場ではどこでどのように型付けしているのか、という世界観をつかむことに集中できます。

それと、学習する時に、いかに適当に雑に頭に入れておくかってことととりあえず書くということをしていれば、とりあえず書けるようになるぶんには、はじめてプログラミングを勉強した時と比べて学習コストが特段高いと感じない気はします。

PureScript興味あるという人は、Haskell と Elmから勉強すると実は近道かもしれません(適当)

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<>で結合するような形になり、見栄えはあまりよろしくないと思うかもしれない。

NodemailerをPureScriptで使えるライブラリつくっといたよ

つくったやつ

GitHub - oreshinya/purescript-nodemailer: Bindings nodemailer for PureScript

どういうライブラリ?

メール送るやつです。 フルスクラッチメーラー書くのめんどくさかったので、内部ではGitHub - nodemailer/nodemailer: ✉️ Send e-mails with Node.JS – easy as cake!を使っています。

使い方

とりあえず現段階ではAPIも少ないし、受け付けるオプションも少ないのでドキュメントを読めばわかると思います。

補足

普通の場合、この返り値つかわねーだろみたいなのは、PureScript側に渡さずに代わりにUnit渡したり、 各種オプション(型シノニム宣言されてるRecordを指している)は、本当はもっと色々受け付けるけど、とりあえず最小限のものしか受け付けないようになってたり、 など、最低限のユースケースを満たす状態でリリースしています。 あとは、私が使っていく中で色々対応するとは思うし、追加対応したいところあったらPR待ってます。

各種PureScriptパッケージを更新した

追記

本記事のパッケージのうち、以下のパッケージ以外はメンテされていません、ご注意ください。

  • purescript-crypto
  • purescript-mysql
  • purescript-simple-emitter

更新したパッケージ

先週は、PureScriptで仮想DOM用のdiff処理を書いた - oreshinyaのブログの続きの作業として、仮想DOM実装への組み込み作業を行なった。 ついでに、ほっぽっていたパッケージのdependenciesの更新と整理を行なった。

Dependenciesを更新したパッケージ

本体コードに変更を入れたパッケージ

purescript-vomについて

purescript-key-based-diffを使ってdiffのとりかたを賢くするようにした。(ようするにパフォーマンスがよくなった。)

組み込みながらpurescript-key-based-diffのインターフェースや処理を使いやすいように調整しつつ行なった。

内部処理が複雑めになるのでテストを書いておいた。 jsdom-globalFFI経由で呼び出して、生DOM部分のテストを書いている。

purescript-mysqlpurescript-conveyorについて

purescript-affという依存パッケージの新しいバージョンに対応した。 このパッケージは、雑に言えば非同期な副作用を扱うモナドで、基本的にAltjsであるPureScriptにとって、とても重要なパッケージなのだが、 これにそれなりな変更が入っていた。 私のほうで行なった対応は、インターフェースの変更に対する対応、そして、AffをFFIとして定義するときの方法やその使い方の変更に対する対応を行なった。

余談

PureScriptは公式であらたなパッケージマネージャをつくっており、その依存解決をpackage-setsを用いて行う。package-setsは、公式の人間やコミュニティの人間がPRをおくってパッケージを加えていくことによって依存解決できるようになる、という形となっている。 そこに、拙作のpurescript-cryptoというパッケージが追加されていて地味に嬉しかった。 いやーしかしPureScript 0.12から入るEffからIOへの移行はいつくるんだろうなー そのときはまたアップデート祭りだー

PureScriptで仮想DOM用のdiff処理を書いた

追記 本記事に出てくるパッケージは現在メンテされていません。 また実装に問題があるため参考にはしないでください。

動機

拙作の仮想DOM実装のpurescript-vomは、child nodeの差分計算が雑に作られていた。 しかし、そろそろ真面目にこのパッケージ使いたいなと思い、その差分計算の実装を少し賢くしようということで実装した。 それがこちらのpurescript-key-based-diffである。 まだpurescript-vomにはこの処理を組み込んでいないが、それはまた後日やる。

どういう感じなのか

馬鹿正直に実装せず、計算量が少なく済むようなリスト変更パターンで処理を進められるだけ進めるという感じになっている。 具体的にはコードを見てもらえればわかると思うが、

  • 先頭から処理を進めていった場合に、先頭が古いリストと同じリストで同じパターン
  • 末尾から処理を進めていった場合、末尾が古いリストと同じリストで同じパターン
  • 先頭から処理を進めていった場合のリストがリバースされているパターン
  • 末尾から処理を進めていった場合のリストがリバースされているパターン

を先にチェックして進めるだけ進めるようにしていて、 進めるだけ進んだら、未処理の分をぐるっとまわして、普通にkeyと要素のマップつくってから、残りの処理をするという感じ。

また、この処理は最終的にリスト操作に応じてdomへの反映処理を行うこととなるので、Effで反映処理をうけつけるようにしている。

感想

処理の性質上、すんごい手続き的な書き方になったが、すごい見栄えが悪いしコードも長かった。(通常はこんなコードにならない)

また、マジレスすると、デフォでカリー化されるPureScriptで書くより、FFI側でJSで全ての処理を書いて、インターフェースだけPureScript側に公開する形をとったほうがパフォーマンスはよいだろう。 しかし、あんまりFFIでどうにかしないようにしたいというお気持ちが強かった....

最後に

PureScriptはユーザーが少ないため、マサカリやPRが基本的にこない。そのため、マサカリ不足となっている。マサカリは能力値アップには必要不可欠であると考えるため、なるべく自分のつくったものを宣伝しようという気持ちができてきている。

GitHubに拙作のPureScriptパッケージがいくつかあるので、優しい人はPRやマサカリをなげていただけるとかなり喜ぶ。