shake + lucid + hint で静的ウェブサイト生成

The English version is at Dev.


同人活動用のウェブサイトがあって今までは Jekyll で生成していました。これを Shake + Lucid + Hint で作成した生成器に置き換えました。

doujin.kakkun61.com

shakebuild.com

hackage.haskell.org

hackage.haskell.org

ソースコードはこちらです。

github.com

経緯

GitHub Pages をホストに選択したので最初は自然に Jekyll を選びました。レールに乗っているうちはいいのですが外れたことをしようとすると難しくなってきました。

どうレールを外れようとしたのかを説明するためにウェブサイトの説明をします。このウェブサイトは自サークルで発行した同人誌の紹介をするもので、同人誌ごとのページとそれを一覧するページから成ります。同人誌は即売会で頒布するためそれぞれの同人誌には即売会の情報が付随します。そうなると、ある即売会でどの同人誌が頒布されたかを表示したくなりました。つまり、即売会ごとのページとそれを一覧するページが欲しくなりました。これを実現するのは Jekyll では難しくありました*1

最初にサイトを作成したのが2018年1月ごろで、その後すぐに別のものに移ろうと Hakyll を触ったりしたのですが Hakyll の API の設計は好きになれず結局放置していました。

それでしばらく移行計画は頓挫していたのですが GHC のビルドシステムに Shake が使われているのを見てこれを使えるのじゃないかと思いました。そして調べたら Shake を利用した静的サイト生成器として RibSlick がありました。

API の好みから Slick を利用してみて、自分に必要のない部分を削いでいったらほとんど Slick がなくなったので Shake を直接使うようになりました。

構成

下記が概要図でこれからこれについて説明していきます。

f:id:kakkun61:20200925234250p:plain

まずコンテンツの変更をするたびに GHC でのコンパイルとリンクをするのは時間がかかるのでやりたくありません。そこで Haskell インタープリターを組み込むことにしました。そのライブラリーが Hint です。

流れとしては、Shake を使ってルールを書き、Hint でインタープリターを埋め込んで、実行ファイル gen を作ります。そして gen を実行してコンテンツの Haskell ソースや画像などを読み込み HTML などに変換して出力します。

Data.hs は gen の生成にも、gen が実行するインタープリターからも使うので両方から読み込みます。

Shake のルールには Make のように成果物を指定して依存解決する「後向き」と、ソースを指定する「前向き」とがあります。今回は前向きを使用しています。「前向き」の場合は別途 Filesystem Access Tracer(fsatrace)が必要です。

実装

ディレクトリー構成は次のようにしました。

  • app
    • gen.hs — 生成器本体
  • content
    • book
      • xxx.hs — 同人誌ごとのページ
    • image
    • lib
      • Layout.hs — コンテンツを囲むレイアウト
    • style
    • xxx.hs — その他のページ
  • lib
  • doujin-site.cabal

gen のさわりはこの辺りです。

lucid :: forall p r. (Show p, Typeable r) => FilePath -> FilePath -> p -> Shake.Action r
lucid source destination param = do
  libs <- Shake.getDirectoryFiles "content/lib" ["*.hs"]
  result <- liftIO $ Hint.runInterpreter $ do
    Hint.set [Hint.languageExtensions := [Hint.DuplicateRecordFields, Hint.OverloadedStrings]]
    Hint.loadModules $ ("content" </> source) : (("content/lib" </>) <$> libs)
    Hint.setTopLevelModules ["Main"]
    Hint.setImports ["Data.Functor.Identity", "Lucid", "Data.Text"]
    Hint.interpret ("render (" ++ show param ++ ")") (Hint.as :: Lucid.Html r)
  case result of
    Left e  -> do
      liftIO $ hPutStrLn stderr $ displayException e
      fail "interpret"
    Right html -> do
      Shake.writeFile' ("out" </> destination) $ show html
      pure $ runIdentity $ Lucid.evalHtmlT html

大して難しいことはしていませんが、1つ制約があって、それは show param の結果が妥当な Haskell コードになっていることです。自動導出していれば問題ないはずですが、自前で show を実装していると動作しないかもしれません。

コンテンツの Haskell ソースは render :: Typeable r => p -> Html r を露出していることにしています。book/xxx.hs は下記の形をしています。

{-# LANGUAGE OverloadedStrings #-}

import           Data
import qualified Layout as L

import Lucid

render path = do
  L.top (L.ogp ogp) $ L.book book (Just content)
  pure book
  where
    ogp = …
    book = …
    content =

返り値の Book はそれぞれのページの分が集められた後 index.hs に渡され一覧ページの作成に使用されます。

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns        #-}
{-# LANGUAGE OverloadedStrings     #-}

import           Data
import qualified Layout as L

import Data.Foldable as F
import Lucid

render (path, books) =
  L.top
    (L.ogp ogp)
    $ div_ [class_ "home"] $ do
        ul_ [class_ "book-list"] $ do
          F.for_ books $ \(Book { title, bookImage, events }, path) -> do
            li_ $ do
              h3_ $
                a_ [class_ "post-link", href_ path] $ toHtml title
              div_ [class_ "justify-bottom"] $ do
                ul_ [class_ "event-badges"] $ do
                  F.for_ events $ \Event { title } -> do
                    li_ [class_ "event-badge"] $ toHtml title
                    " "
                a_ [href_ path] $ img_ [src_ bookImage, alt_ "book image", class_ "home-book-front"]
  where
    ogp =

HTML の記述には Lucid を使用しました。内部 DSL なのでHaskell コードと統一した記法で埋め込めるので楽です。ここは好みで取り替えができます。

ここまで来れば即売会ページとその一覧ページを作るのも簡単そうです。

まとめ

  • Shake を依存関係記述に使う
  • Hint でインタープリターを埋め込む
  • Lucid で HTML を記述する
    • EDSL なので統一した記法で書ける

感想としては、型にはまったフレームワークでコンテンツを書くよりも、パーツとしてのライブラリーを組み合わせて作る方が柔軟性が高くて好みだなと再確認しました。

追記

2020.09.27

テンプレートリポジトリーにしました。

github.com

*1:少なくとも当時の Jekyll では。

「共有した URL を別のアプリで開く」2.0 をリリースしました

play.google.com

どういうアプリかというと EXTRA_TEXTURI に入ったインテントACTION_SEND でやってくると ACTION_VIEWインテントを投げ直すアプリです。

地味に50万超インストールされて、なぜかブラジルでよく使われているようです。

バージョン 2.0 は実に6年ぶりのアップデートでした。

今回の変更点は下記の通りです。

  • minSdkVersion: 9 → 17
    • Android 2.3 ~ 4.1 がサポート対象外に
  • targetSdkVersion: 21 → 29
  • Intent.createChooser() を使わず自前表示
    • 詳細は後述
  • ローンチャー(ホーム)にアプリアイコンを追加
    • 起動のしかたが分からない人がいて、低評価されるので
    • オプションで非表示にできる

「デフォルトのアプリ」や「アプリリンク」

Android のいつのバージョンからか「デフォルトのアプリ」という機能が増えています。設定アプリで〔アプリと通知〕→〔デフォルトのアプリ〕にあります。

f:id:kakkun61:20200923154958p:plain

これでブラウザアプリとして Chrome などを指定していると Intent.createChooser() でも PackageManager.queryIntentActivities() でもそれしか出てきません。

アプリリンクの場合も同じでアプリリンク用のアプリだけが選択肢に出てきます。

これを回避してインテントを受け取れるアプリ全てを取得する方法をご存じなら教えてほしいです。

Intent.createChooser() から PackageManager.queryIntentActivities() に変えれば取得できるかと思って変えたのですが同じでした。ただ選択肢が1つのときに Intent.createChooser() ならそのままアクティビティーが起動してしまうので、自前表示して選択肢を表示したかったので PackageManager.queryIntentActivities() に変えました。

今の実装の該当コードはこの辺りです。

        PackageManager pm = getPackageManager();
        Intent urlIntent = new Intent(Intent.ACTION_VIEW, uri);
        List<ResolveInfo> resolveInfos = pm.queryIntentActivities(urlIntent, 0);

英語に問題なければこちらに回答いただければ嬉しいです。

stackoverflow.com

鬱になりました

鬱になりました。

TwitterFacebook にはちらっと書いたのですが、で1月終わりから休職しています。ここらで一度ふりかえってみようかと思ったので書いてみます。

f:id:kakkun61:20200904160823j:plain

kokoro.mhlw.go.jp

何かおかしい

何かおかしいなと思ったのは正月の帰省から戻ってきた後でした。買い物に行きたいなと思いつつ全然部屋から出る気が起きず4日ぐらい1日スパゲッティ1食みたいな生活でした。元々出不精とはいえ出たいと思いつつ出れないのが続いてるなと思っていました。

その後しごとが始まりそれなりにしごとをしていましたが、朝の起きれなさ*1と起きても倦怠感や外に出たくなさがあり在宅勤務をする日が増えてきました。在宅勤務でしごとがはかどればよいのですが、今いちしごとに手が着かず週次の進捗報告で報告する進捗が減っていくのを感じていました。

さすがにこれはしごとをやっているとは言えないなという気持ちになり、出社する足で会社に行かずに精神科病院に行きました。

精神科病院が家の向かいであることはさいわいだったと思います。そうでなければグダグダと行く予定を先延ばしにしていたはずです*2

診察後上司にチャットでしばらく休職したいむねを報告しました。

休暇 1ヶ月め(2月)

最初の1ヶ月は積極的なことは何もせず休養をしていました。

意欲を完全に喪失していたので趣味のプログラミングもせずひたすらビデオを見ていました。『エウレカセブン』や『おジャ魔女どれみ』を全話見たりしました。

意欲を失った結果夜起きていてもしかたがないのでこのころ睡眠の周期がよくなりました。しかし日中もよく寝ていました。

この時期は意欲の喪失だけでなく思考能力もかなり落ちていました。プログラミングをしなかったのは意欲低下だけでなく思考能力の低下も理由にあります。

1ヶ月も休んだしそろそろ復職するかと人事や産業医に伝えたところ、まだ休職できるので十分休養しなさいということで休職を続けることにしました。後から思うと自分でもまだまだ休養すべきだったと思います。

休暇 2・3ヶ月め(3・4月)

鬱になってから気付いたのですが自分の場合心が凹むと部屋が乱雑になってくるようです。特に台所にそれが一番初めに表れます。心の凹みは注意しないと見逃してしまうので台所の乱雑さは自分の指標だと思うようになりました。

意欲が少しずつ回復してきたので暇を感じるようになりました。また少しは先のことを考えるようになりました。医者に相談したところ復職支援・再発防止のリハビリテーションがあるということでそれに参加するようにしました。それはリワークと呼ぶそうです。最終的には週に5日リワークに通うことが1つの目標です。

リワークでは、詳しいことは分かっていないのですが、認知再構成法や認知行動療法問題解決療法といった手法を学んだり、他の参加者とボードゲーム*3をすることを通じて、思考や対人コミュニケーションの訓練をしたりします。またヨガや創作活動によって悩みなどを一時的に忘れるようにしたりします。

趣味の面ではこのころ鉄道模型ジオラマを作っていました。手を動かす作業をするとイキイキした感じがしました。以前から気になっていた 3D プリンターをジオラマに使いたくなったので光造形式 3D プリンターを購入しました。いろいろとしていましたがプログラミングはまだひかえていました。

どれくらいプログラミングができていなかったのかプログラマーには分かりやすい図だと思うのですが GitHub のアクティビティーの図を下に示します。3月にちょろっと活動があるのは 3D モデルを GitHub にアップロードしたからです。

f:id:kakkun61:20200904140104p:plain

休暇 4・5ヶ月め(5・6月)

このころになるとちょこちょこプログラミングをし始めるようになりました。そしてその結果夜更かしが増えました。これはいけません。プログラミング、うまく問題が解決できるととても快楽を感じるのですが、なまじそれを知ったがためにもうちょっとでできそうというような気持ちでついつい夜更かししてしまいます。これはしごとでもそうで夕方から気分が乗ってきた結果、職場の電灯が消える時間までやってしまう傾向がありました。これは、直すべきだとこの休養中に認識したことの1つです。

夜更かしについてもそうですが、リワークの中でストレスを受けた状況をふりかえることがあります。そこで自分が課題であると認識したことの1つに対人コミュニケーションに苦手意識があるということです。しごとやその他において何か相談ごとがあるときに、うまく人に話しかけられない、話しかけるのに苦痛を感じるというのがあります。よくよくふりかえると今回の休職の原因にも関係しているように思えています。これは長い付き合いになると思いますがうまくやっていけるよう克服したいと思っています。いろいろと仮説や対策などを考えているのですが長くなるのでここでは割愛しましょう。

金銭面ですが、かなり貯金を切り崩してきました。傷病手当で給与の3分の2に相当するものがもらえるのですが該当期間の3・4ヶ月おくれということもあって現金がゴリゴリと減っていきました。生活するだけでかなりお金が必要なのだなと実感します。一部株式を手放しましたが、COVID-19 で一時下がっていた株価が持ち直していたので少し助かりました。

休暇 6・7ヶ月め(7・8月)

どうも気持ちには波があるようです。この時期は負の時期で睡眠周期が過去にないほど崩壊しました。昼夜逆転だけでなく長時間寝たり短時間を1日に何回も寝たりといったぐあいでした。そんな状況だったのでリワークにもほとんど通えず、進捗リセットという感じでそのことに凹みます。傷病手当申請と同時に会社に生活記録を提出する必要があるのですが、こうした時期は事務しごとがなかなか進まず申請が2ヶ月ほど遅れてしまいました。その影響でまた現金がギリギリです。

休職できる期限があるのでいつまでも休職するわけにはいかないのですが、復職するまでにまだ克服したいことが残っているのでしばらくはその訓練をすることになりそうです。

まだ書くことがあった気がしますが、公開しないと思い出せそうにないので公開してしまいます。

最後に

物的支援をよろしくお願いします!

ウィッシュリスト(食品・日用品)

ウィッシュリスト(趣味)

*1:元々睡眠障害をわずらっていましたが。

*2:普段から散髪など基本的に外出予定はズルズルと先延ばしになるので。

*3:お邪魔者』がおもしろかったです。

一番簡単な MonadFail インスタンス

The English version is at Dev.


導入

failMonad から剥がされて早や幾年、私は失敗する可能性のある計算は MonadFail を使って型を付けるのが好きです。

foo :: MonadFail m => m a

こうすると IO の文脈であればその中で、純粋な文脈であれば Maybe などで具体化して呼ぶことができます。

-- IO の文脈では
foo :: IO a

-- 純粋な文脈では
foo :: Maybe a

さて、純粋な文脈として Maybe を使うと失敗のメッセージを失ってしまうことが嬉しくありません。では、Either を使えばいいのではないでしょうか?実は EitherMonadFailインスタンスになっていません。提案はされていますが、失敗・成功以外に同列にパラメーターを扱うケースもあるのでそのときに MonadFail であることは適切でないからです*1

gitlab.haskell.org

そういうわけで一番簡単な MonadFail インスタンスとして次のような Result 型が欲しくなりました。

newtype Result a = Result (Either String a)

instance MonadFail Result where
  fail = Result . Left

実をいうとこれに相当するものはすでにあるのですが非推奨となっています。それは mtl パッケージの ErrorT です。

either-result パッケージ

そういうわけで Result に加えいくつかの関数をまとめてリリースしたのが either-result パッケージです。

hackage.haskell.org

実際には Resultモナドトランスフォーマー版の ResultT を使って実装され、ResultT は transformers パッケージの ExceptTnewtype です。

type Result a = ResultT Identity a

newtype ResultT m a = ResultT (ExceptT String m a)

ResultTExceptT と異なるのは MonadFail インスタンスで、fail を呼ぶと ResultTLeft でくるむのに対して ExceptT ではベースのモナドfail を呼びます。ですので、ResultT ではベースのモナドMonad しか要求しませんが、ExceptT では MonadFail であることを要求します。

instance Monad m => MonadFail (ResultT m) whereinstance MonadFail m => MonadFail (ExceptT e m) where

モナドトランスフォーマーにしたついでに mtl の MonadError インスタンスにもなっているので throwErrorcatchError することができます。

exceptions パッケージは?

MonadThrow という型クラスもなかったっけ?はい、あります。exceptions パッケージ*2MonadThrowManadCatch 型クラスがあります。こちらは投げる・捉えるものが Exception 型クラスであることを要求します。使い分けとしては、投げる・捉えるものを型で区別したい場合は MonadThrowMonadCatch にして、単にメッセージのみでよい場合は MonadFail にすればよいと思います。

class Monad m => MonadThrow m where
  throwM :: Exception e => e -> m a

class MonadThrow m => MonadCatch m where
  catch :: Exception e => m a -> (e -> m a) -> m a

class Monad m => MonadFail m where
  fail :: String -> m a

まとめ

  • 定義時、失敗する計算は MonadFail m => m a にしよう
  • 使用時、IO などの文脈では IO a などとして使おう
  • 使用時、純粋な文脈では Result a として使おう
  • GitHub リポジトリーの Star というボタンを押そう

*1:Rust ではデフォルトで Result 型で同列に扱いにくいため Either という名前がよかったという主張もあるみたいですね。

*2:使用する場合は safe-exceptions パッケージをおすすめします。

フィルムの在庫を数えた

SARS-CoV-2 の影響やらで最近フィルムの消費が少ないので期限切れフィルムを洗い出すことと冷蔵庫の中の整理のためにフィルムを全部出して数えてみた。

続きを読む

Twitter ツイートにウェブアプリを埋め込む

Twitter Card の Player だと iframe を使って埋め込めることを知ったので埋め込んでみた。(ツイートの埋め込みの中で埋め込んだものを再生できないので一旦ツイートを開く必要がある。)

f:id:kakkun61:20200728163346p:plain

Player Card を使ってるのはよく見るのは YouTube だろう。WebGL を埋め込んでる人もいた。

実装

実装としてはメタタグを埋め込むだけ(diff)。

    <meta name="twitter:card" content="player" />
    <meta name="twitter:site" content="@kakkun61" />
    <meta name="twitter:title" content="Photo Film Dev" />
    <meta name="twitter:description" content="This is a timer app for developing monochrome negative photograph films." />
    <meta name="twitter:image" content="https://photo-film-dev.kakkun61.com/preview.png" />
    <meta name="twitter:player" content="https://photo-film-dev.kakkun61.com/index.html" />
    <meta name="twitter:player:width" content="400" />
    <meta name="twitter:player:height" content="650" />

埋め込むコンテンツにはルールがあるので注意しよう。

developer.twitter.com

Elm でマテリアルデザインなウェブアプリを作った

アプリ概要

アプリとしてはニッチな用途で、写真用のモノクロネガフィルムを自家現像するときに便利なタイマーアプリです。下記は埋め込まれた実際に動くアプリです。

Photo Film Dev で公開しています。

時間配分をレシピと呼んでいますが、レシピはサーバーに保存されるためログインすれば複数の端末でアプリを使用できます。

今はレシピは自分のアカウントでしか見れないですが別のアカウントに共有する機能が作れたらなと考えています。

技術構成

ソースコードGitHub に GPL3 で提供しています。

github.com

アプリの技術構成としては次の通りです。

Elm

Elm は言語としては Haskell 様の文法を採用した alternative HTML/CSS/JS です。The Elm Architecture がいい感じです。チュートリアルを読めば書けるようになります。

モジュール分割

最初は1ファイルに全部書いて始めたのですがわりとすぐにどの辺りに何を書いたか分からなくなったのと同じ名前を付けたいものが出てきたのでモジュール分割をしました。詳細は後述します。

モジュール 役割
Model Msg 以外のデータ型
データ同士の変換関数など
Model.Foo enum として定義する Foo
getset など関連する関数
Msg Msg のデータ型
Msg.Foo 入れ子になる Msg のデータ型
update 関数と対応する
Port port 関数とその引数になる型
その型と内部で使用する型との変換関数
Cmd Port で定義した関数を組み合わせた Cmd を生成する関数
Sub Port で定義した関数を組み合わせた Sub を生成する関数
Update 直和型として定義した Msg 型のそれぞれの値構築子に対応する update 関数
View 画面要素ごとに分割した view 関数
Text 多言語テキスト

Enum

Elm にはアドホック多相が組み込み関数にしかありません。例えば Step という段階を表す型を作って Haskell であれば Enum 型クラスにするところですができないのでモジュールを分けて toIntfromIntcompare などを定義することになります。標準ライブラリーの型でも Maybe など Monad になるものはモジュールごとに andThen を定義しているので正当な方法だと思います。

module Model.Step exposing (..)

type Step
    = Soak
    | Dev
    | Stop
    | …

toInt : Step -> Int
toInt step =
    case step of
        Soak -> 0
        Dev -> 1
        Stop -> 2
        …

fromInt : Int -> Maybe Step
fromInt value =
    case value of
        0 -> Just Soak
        1 -> Just Dev
        2 -> Just Stop
        …
        _ -> Nothing

compare : Step -> Step -> Order
compare step0 step1 = Basics.compare (toInt step0) (toInt step1)

Dict 様レコードでの DRY

フィールドの型が全部同じで辞書(マップ)として使用するような下記のようなレコードの場合フィールドごとに同じような処理を書こうとしてもそのままでは実装を複製するしかありません。Dict にする手もありますが存在することが分かっているのに不必要な Maybe だらけになってしまいます。

type alias TimeSpans =
    { soak : TimeSpan
    , dev : TimeSpan
    , stop : TimeSpan
    , fix : TimeSpan
    , rinse : TimeSpan
    , wet : TimeSpan
    }

そこで先の enum として作った型をキーとして getset 関数を作ります。

module Model.Step exposing (..)

…

get : Step -> { soak : a, dev : a, stop : a, fix : a, rinse : a, wet : a } -> a
get step =
    case step of
        Soak -> .soak
        Dev -> .dev
        Stop -> .stop
        …

set : Step -> a -> { soak : a, dev : a, stop : a, fix : a, rinse : a, wet : a } -> { soak : a, dev : a, stop : a, fix : a, rinse : a, wet : a }
set step value record =
    case step of
        Soak -> { record | soak = value }
        Dev -> { record | dev = value }
        Stop -> { record | stop = value }
        …

これで下記のようにアクセスできます。

timeSpans |> get step
timeSpans |> set step value

これで don't repeat yourself にのっとって処理をまとめることができます。

Msgupdate の分割

Msgupdate の分割のしかたは対応させています。

-- Msg.elm
type Msg
    = Drawer Visible
    | SelectRecipe Recipe
    | GoRun
    …

-- Main.elm
update : Msg -> Model Msg -> ( Model Msg, Cmd Msg )
update msg model =
    case msg of
        Msg.Drawer visible -> Update.drawer visible model
        Msg.SelectRecipe recipe -> Update.selectRecipe recipe model
        Msg.GoRun -> Update.goRun model
        …

Msg.Drawer には Update.drawer というように対応させています。入れ子になったメッセージである VisibleMsg.Visible モジュールに定義してあります。Model の状態による条件分岐は各々の update の中で行っています。

PortCmdSub

Model モジュールに Recipe は下記のような型で定義しているのですがこれは port 関数で JavaScript に渡したり JavaScript から渡されたりすることができません。なので Port モジュールに同名の Recipe を作り変換関数も Port に定義しました。

-- Model.elm
type alias Recipe =
    { id : UUID
    , name : String
    , timeInputs : TimeInputs
    }

-- Port.elm
type alias Recipe =
    { id : String
    , name : String
    , timeInputs : Model.TimeInputs
    }

Port.Recipe から Model.Recipe への変換には elm-form-decoder を使いました。フォームからの変換以外でも外界からデータを取り込む場合に便利です。

Port.setRecipeCmdPort.Recipe に依存していますが Cmd.setRecipeModel.Recipe に依存するように Port の変換関数を使って変換しています。Sub.changeRecipe も同じく Port.changeRecipeSub から変換していますが、msg の構築子が2つあるのは変換の成功時・失敗時を表現するためです。

-- Port.elm
port setRecipeCmd : Recipe -> Cmd msg -- ここの Recipe は Port.Recipe

-- Cmd.elm
setRecipe : Recipe -> Cmd msg -- ここの Recipe は Model.Recipe
-- Port.elm
port changeRecipesSub : (Array Recipe -> msg) -> Sub msg -- ここの Recipe は Port.Recipe

-- Sub.elm
changeRecipes : (List RecipeDecoderError -> msg) -> (Dict String Recipe -> msg) -> Sub msg -- ここの Recipe は Model.Recipe

Material Design

Material Design を採用しライブラリーとしては elm-mdc を使用しました。UI センスがなくても Material Designガイドラインにしたがえばそれっぽくなるので便利です。

github.com

Elm のライブラリーは Elm しか内包できません。このライブラリーは Elm の他に JavaScript なども含むため Git リポジトリーとして公開され、サブモジュールとして依存します。

elm.jsonpackage.json は下記のようになります。

// elm.json
{
    "type": "application",
    "source-directories": [
        "src",
        "elm-mdc/src"
    ],
    "elm-version": "0.19.1",
    "dependencies": {},
    …
}
// package.json
{
  "dependencies": {
    "elm-mdc": "file:elm-mdc",
    …
  },
  …
}

ビルドタスクは GNU Make で管理しているのですが Parcel を使用しているにもかかわらず上記事情のため elm-mdc の面倒も見ているので少々複雑になっています。

多言語テキスト

Text モジュールでは下記の型の値を列挙しています。といっても今のところ日本語のみですが。

type alias Text = Language -> String

モジュール依存関係

モジュールの依存関係を下図に示します。Text は図から省略しています。

f:id:kakkun61:20200720040856p:plain
モジュール依存関係

Text は他のいずれにも依存しません。Model グループは Text にのみ依存します。黄色で ModelMsg を囲っているものは単に矢印の数を削減するためのもので、黄色枠への矢印もしくは黄色枠からの矢印はその中の1個以上への矢印もしくはその中の1個以上からの矢印と読んでください。MsgModelText にのみ依存します。他に特記すべきは PortCmdSub のみから依存され必ずラップされたものが他から参照されます。

Firebase

サーバーは自前で書くつもりだったのですが Firebase をさわってみたら認証とストレージだけで十分だったのでサーバーは自前では立てないことになりました。local storage を使ってスタンドアローンで動くところまで作っていましたが Firestore がオフラインの面倒も見てくれるので local storage の実装は消しました。

先に書いたように Elm のライブラリーは Elm のみに限定されるので、必然的に JavaScript に依存する Firebase のラッパーライブラリーはありません。とはいえ書くのはグルーコードなので大した問題ではありません。

クレジットページ

頒布をともなう場合、多くのオープンソースライセンスはコピーライト表記とライセンス表記を求めます。なのでクレジットページを作成しました。これは自動生成されるようにしています。NPM License CheckerElm License Checker を使用して生成しています。Elm License Checker はなかったので作りました。(Elm License Checker は PureScript 製です。作成時 Elm で CLI を作れることを知らなかったので。)

www.npmjs.com

謝辞

Discord の Elm-jp サーバーのみなさまにはお世話になりました。