Haskell を用いたアプリケーション開発は
「新しく作るのには時間がかかってしまうが、その代わり強い静的型のおかげで保守性が高い」
と言われることがよくあります。
もちろん「いや、新しく作る際にもむしろ型のおかげですばやく作れる」などの反論もありますが、こういったトレードオフがあることもまた事実です。
さらに、Haskellの闇の力に飲まれてしまった方が書く厨ニ病コードは、本人すらも1週間後には意味がわからなくなって保守性すら低くなる怖さも秘めています。
今回ご紹介する Tonatona は、Haskellを用いたアプリケーション開発にありがちなこういった問題を解決して、今までの常識を覆す「統合的アプリケーションフレームワーク」です。
公式リポジトリ
最新の内容はTonatonaのリポジトリにあります。
トナカイのコスプレをしたさくらちゃんが目印です。
どんな人のためのフレームワークか
Tonatona は現実世界で実用的なアプリケーションを開発する上で障壁となる問題を解決するために、株式会社ARoWでの知見をもとに作成されたフレームワークです。
それを実現するために、上記の「闇の力」に繋がりうるHaskellの学術的なおもしろさを一切排除してあります。
そういった難しい部分を全部フレームワークの内部に閉じ込め、Tonatonaを利用する開発者から闇の力を使う自由を奪っています。
将来的には Haskell を全く知らない開発者にも使えるようにすることを考えており、むしろTonatonaを使うと開発が平凡でつまらないものになるはずです。
以上の理由から、次のような方には Tonatona はまったくオススメできません。
- すでに闇の技術を使いこなしていてまったく困っていない方
- 「Haskell使いこなしている俺かっけー」と闇の技術を使いこなしてエクスタシーを感じることに価値を置く方
- Haskell の学術的な側面を支えて Haskell の発展に貢献してくださることで活躍されている方
- 「ルールに従うのではなく、俺がルールを作るのだ!」という価値観の方
これらの方は「読んで損した!」と思う前に、このページを閉じていただくことで人生の貴重な時間を節約できます。
なぜ 統合的アプリケーションフレームワークなのか
Tonatona を「統合的アプリケーションフレームワーク」と呼ぶ理由は、他のアプリケーションフレームワークをプラグインのように組み込んで使うことを想定しているからです。
たとえばウェブアプリケーションフレームワークである Servant を組み込むことで、Servant の手が届かない部分を変わりに Tonatona が行い、Servant の良さを120%引き出すことができます。
Servant は「美しい」フレームワークですが
- 開発環境/本番環境 などに応じてログレベルを変更したり
- 環境変数に応じてサーバーのポート番号を変えたり
- DBアクセスのライブラリと連携したり
といった娑婆世界の汚辱にまみれた部分は開発者が自分でコードを書かないといけません。
Haskell のライブラリには高潔で神聖な世界のみを救うものも多いですが、
Tonatona はこういった泥まみれな世界にも顕現して衆生を救ってくれるのです。
まずは試してみよう!
だらだら説法を続けられても眠くなるだけです。
抽象的な話はここまでにして、さっそく Tonatona を使ってみましょう。
Tonatona を使ったプロジェクトを始めるのはとっても簡単です。
stackさえ使えるようにしておけば、あとは Tonatona用のテンプレートを使ってプロジェクトを初期化できます。
$ stack new sample-app https://raw.githubusercontent.com/tonatona-project/tonatona/master/tonatona.hsfiles
このコマンドを実行すると、sample-app
という名前のディレクトリが作成されるはずです。
作成された sample-app
ディレクトリの構成は以下のようになっています。
sample-app
├── app
│ └── Main.hs
├── LICENSE
├── package.yaml
├── README.md
├── Setup.hs
├── src
│ └── TonaApp
│ └── Main.hs
├── stack.yaml
└── test
├── DocTest.hs
└── Spec.hs
開発時に実際に触るのは package.yaml
と src/TonaApp/Main.hs
がメインです。
まずは src/TonaApp/Main.hs
の中身を見てみましょう。
module TonaApp.Main where
import Tonalude
import Tonatona (HasConfig(..), HasParser(..))
import qualified Tonatona.Logger as TonaLogger
-- App
app :: RIO Config ()
app = do
-- Tonatona.Logger plugin enables to use logger functions without any configurations.
TonaLogger.logInfo $ display ("This is a skeleton for tonatona project" :: Text)
TonaLogger.logDebug $ display ("This is a debug message" :: Text)
-- Config
data Config = Config
{ tonaLogger :: TonaLogger.Config
-- , anotherPlugin :: TonaAnotherPlugin.Config
-- , yetAnotherPlugin :: TonaYetAnotherPlugin.Config
}
instance HasConfig Config TonaLogger.Config where
config = tonaLogger
instance HasParser Config where
parser = Config
<$> parser
-- <*> parser
-- <*> parser
たったこれだけですが、実はこれだけで結構おもしろいアプリケーションになっています。
少しずつ謎を解き明かしていきましょう。
まず見て分かるのが、tonaludeライブラリを読み込んでいることです。
tonatona
は Tonalude
を Prelude
の代替として使っています。
import Tonalude
Tonalude は「Prelude に定義されている関数の IO a
を RIO env a
に置き換えて、その他罠がある関数を安全にし、tonatona用にわかりやすくしたもの」です。
IO a
の代わりに使っている RIO env a
型について詳しく知る必要はありません。
「なんかちょっと IO a
よりもいい感じのほとんど IO a
のやつ」と思っておけばOKです。
次の import
を見てみましょう。
import Tonatona (HasConfig(..), HasParser(..))
import qualified Tonatona.Logger as TonaLogger
Tonatona
が、Tonatona のメインのモジュールです。
Tonatona.Logger
が、Tonatona が用意している Logging 用の「プラグイン」です。
実は Tonatona は「プラグインアーキテクチャ」を採用しており、プラグインを追加するだけで 設定なしに その機能を使えてしまうのです。
このプラグインアーキテクチャの強力さは、読み進めていくとすぐにわかります。
その凄まじさに戦慄することでしょう。
Tonatona では 慣習 として、プラグインのインポートは qualified
インポートにして、TonaXXXXXX
という形式の別名を付けます。
次に、メインのコードです。
app :: RIO Config ()
app = do
-- Tonatona.Logger plugin enables to use logger functions without any configurations.
-- (訳: Tonatona.Logger プラグインのおかげで、何の設定も記述せずに Logging 関数が使えます!)
TonaLogger.logInfo $ display ("This is a skeleton for tonatona project" :: Text)
TonaLogger.logDebug $ display ("This is a debug message" :: Text)
Tonalude
を使っているため、メインの処理を記述するコードも IO ()
ではなく RIO Config ()
という型を持っています。
でも、前述の通り使い勝手としてはほとんど IO ()
と変わりません。
Tonalude
には Prelude
で IO a
を返す関数の RIO config a
版が用意されていますから、特に不自由することはないでしょう。
logDebug や logInfo は Tonatona.Logger
プラグインに用意されている関数です。
logDebug :: HasConfig env Config => Utf8Builder -> RIO env ()
logInfo :: HasConfig env Config => Utf8Builder -> RIO env ()
本来、ロギング用の関数を使うためにはいろんな「儀式」が必要です。
儀式の例を見てみる
以下のコードの意味を理解する必要はないです。「こんな生贄の儀式がいるのか」と本来の苦労を知ってもらうのが目的です。
たとえば、rio
という ライブラリではロギング用の関数を使うためには以下のような「儀式」が必要です。
main :: IO ()
main = do
let isVerbose = False -- get from the command line instead
logOptions' <- logOptionsHandle stderr isVerbose
let logOptions = setLogUseTime True logOptions'
withLogFunc logOptions $ lf -> do
let app = App -- application specific environment
{ appLogFunc = lf
, appOtherStuff = ...
}
runRIO app $ do
logInfo "Starting app"
myApp
class HasConfig env where
configL :: Lens' env Config
instance HasConfig Config where
configL = id
data Env = Env { envLogFunc :: !LogFunc, envConfig :: !Config }
class (HasLogFunc env, HasConfig env) => HasEnv env where
envL :: Lens' env Env
instance HasLogFunc Env where
logFuncL = lens envLogFunc (\x y -> x { envLogFunc = y })
instance HasConfig Env where
configL = lens envConfig (\x y -> x { envConfig = y })
instance HasEnv Env where
envL = id
-- And then, at some other part of the code
data SuperEnv = SuperEnv { seEnv :: !Env, seOtherStuff :: !OtherStuff }
instance HasLogFunc SuperEnv where
logFuncL = envL.logFuncL
instance HasConfig SuperEnv where
configL = envL.configL
instance HasEnv SuperEnv where
envL = lens seEnv (\x y -> x { seEnv = y })
これをまったく意識せずにロギング用の関数を使えるというだけでも、Tonatonaの強力さが分かると思います。
Tonatonaはこの黒魔術のコードを書く代わりに、次のようなヤギにも分かるようなボイラープレートを記述するだけで、闇に飲み込まれる心配もなく安全に力を手に入れることができるのです。
data Config = Config
{ tonaLogger :: TonaLogger.Config
-- , anotherPlugin :: TonaAnotherPlugin.Config
-- , yetAnotherPlugin :: TonaYetAnotherPlugin.Config
}
instance HasConfig Config TonaLogger.Config where
config = tonaLogger
instance HasParser Config where
parser = Config
<$> parser
-- <*> parser
-- <*> parser
全部で3つのパートからなっており、まず最初の Config
型で「どのプラグインを使うか」を Tonatona に指定します。
次の HasConfig
のインスタンス宣言で、「実際にどのプラグインが Config
のどのフィールドにあるのか」を明示します。
最後に HasParser
のインスタンス宣言で、実際に Tonatona が各プラグインの機能を使えるようにします。
コメントが暗示しているように、他のプラグインを追加する時も何も難しいことを考える必要はありません。
-
Config
型にそのプラグイン用のフィールドを追加して - そのプラグイン用の
HasConfig
のインスタンス宣言を追加して -
HasParser
のインスタンス宣言に1行<*> parser
を追加する
たったこれだけです。
トナカイのコスプレをしたヤギにもできそうです。
Haskell好きな人はこういうものを「めんどうだ」と感じる方が多いですが、打たないといけない文字数が増えることがそんなに問題なのでしょうか。
エディタの機能で工夫したらこんなの大したことないはずです。
この文字数が増える負担と、これを解決するためだけに闇に足を突っ込んで「ああ〜〜〜!コンパイルが通らんのじゃぁあああ!!!」と発狂するのとを比較した際に、前者を迷わず選ぶのが Tonatona の方針です。
でも、きっとどこかのバージョンアップで derivePlugin
みたいなおまじないが追加されて文字を打つ負担も減ると思います。
それまではエディタを使いこなしてどうにかしてください。
ここまででコードの説明は終了です。
コードをコンパイルしましょう。
$ stack install --pedantic
これで無事にサンプルアプリが動くはずです。
$ stack exec sample-app
2018-11-18 21:15:09.594168: [info] This is a skeleton for tonatona project
@(src/TonaApp/Main.hs:16:3)
2018-11-18 21:15:09.594783: [debug] This is a debug message
@(src/TonaApp/Main.hs:17:3)
おめでとうございます! ちゃんと動きました。
でも、実は Tonatona のすごさに驚くのはまだ早いんです。
-h
オプションを付けて実行してみましょう。
(stack
に渡すオプションではなく、sample-app
に渡すオプションなので、--
をはさむ必要があります。)
$ stack exec sample-app -- -h
Application deployment mode to run
Default: Development
Type: DeployMode (Development|Production|Staging|Test)
Command line option: --env
Environment variable: ENV
Make the operation more talkative
Default: False
Type: Bool (False|True)
Command line option: --verbose
Environment variable: VERBOSE
Display this help and exit
Default: False
Type: Bool
Command line option: -h
Command line option: --help
わーお!
いつの間にこんな機能が?
なんと、自動でヘルプを表示する機能が実装されています。
しかも、どうやら一部の設定は環境変数でもコマンドラインオプションでも指定できるようです。
ためしに Env=Production
という環境変数をセットしてみましょう。
$ ENV=Production stack exec sample-app
This is a skeleton for tonatona project
すごい! 本番環境用にデバグログが全部消えました。
本番環境で、試しにログを全部出したい時はどうしたらいいのでしょうか?
ヘルプテキストにある --verbose
オプションを渡してみます。
$ ENV=Production stack exec sample-app2 -- --verbose
2018-12-02 18:05:10.444735: [info] This is a skeleton for tonatona project
@(src/TonaApp/Main.hs:16:3)
2018-12-02 18:05:10.445249: [debug] This is a debug message
@(src/TonaApp/Main.hs:17:3)
まじで? 魔法じゃん!
いいえ、これが Tonatona のプラグインアーキテクチャが持つ真の力なのです。
環境変数やコマンドラインオプションから設定を引っ張るようにしているから、コードに何の設定も書かないで済むんですね!
この機能を実現しているのは tonaparser ライブラリですが、そういう苦労を外には見せずに粛々と仕事をこなしています。
本当に洗練された設計は、難しい部分を全部内部に隠すので、一見平凡でつまらなく見えるものです。
プラグインを追加してみる
実際に Tonatona を使ってアプリケーション開発をする際には、他に必要なプラグインを追加していきます。
ここでは sqlite データベースにアクセスするためのプラグインである tonatona-persistent-sqlite プラグインを追加します。
tonatona-persistent-sqlite
はその名が暗示する通り、persistent ライブラリを使ってDBアクセスを記述するのをサポートしてくれるプラグインです。
package.yaml に依存ライブラリを追加する
まず、package.yaml
の dependencies
に tonatona-persisntent-sqlite
ライブラリと、実際にDBアクセスを記述するために persistent
関連の2つのライブラリを追加します。
dependencies:
- base >= 4.7 && < 5
# `persistent` and `persistent-template` are also needed to
# actually use `tonatona-persistent-sqlite`.
- persistent
- persistent-template
- rio
- tonatona
- tonatona-logger
# new plugin to add
- tonatona-persistent-sqlite
プラグインを読み込む
次に、src/TonaApp/Main.hs
に Tonatona.Persist.Sqlite
を import
します。
import qualified Tonatona.Persist.Sqlite as TonaDb
この import 宣言で TonaDb.*
という形式でこのモジュールで定義されている関数を使えるようになりました。
Config
型にフィールドを追加しましょう。
data Config = Config
{ tonaLogger :: TonaLogger.Config
, tonaDb :: TonaDb.Config
}
あとは機械的に HasConfig
のインスタンス宣言をして、
instance HasConfig Config TonaDb.Config where
config = tonaDb
HasParser
のインスタンス宣言に1行 <*> parser
を追加するだけです。
instance HasParser Config where
parser = Config
<$> parser
<*> parser
これだけで、persistent
の get
や delete
を使うための TonaDb.run
が使えるようになります。
ライブラリ特有の準備
ここまでで準備は完了したので、実際にDBアクセスをするようにコードを書いていきましょう。
最初にDBテーブルの設計をします。
ブログ記事を想定したテーブルで、id
, author_name
, contents
の3つのフィールドを持っています。
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
import Database.Persist.TH (mkMigrate, mkPersist, persistLowerCase, share, sqlSettings)
$(share
[mkPersist sqlSettings, mkMigrate "migrateAll"]
[persistLowerCase|
BlogPost
authorName Text
contents Text
deriving Show
|]
)
これはあくまで persistent
のための記述です。
Tonatona.Persist.Sqlite
自体が必要としているものではありません。
実際の処理を記述する
ここまでの準備でいきなり実際にDBへの操作を書けるようになっています。
- DBがどこにあるかとか
- コネクションプールをどうしようかとか
そういうコードは一切必要ありません!
import Database.Persist (insert_)
app :: RIO Config ()
app = do
TonaLogger.logInfo $ display ("This is a skeleton for tonatona project" :: Text)
TonaLogger.logDebug $ display ("Migrating DB..." :: Text)
-- DBにテーブルを作成
TonaDb.runMigrate migrateAll
TonaLogger.logDebug $ display ("Running DB query..." :: Text)
-- `TonaDb.run` の中で `persistent` のDB操作関数が使えます
TonaDb.run $ do
-- `TonaDb.run` の中でも、`lift` を使うことで他のプラグインの機能をそのまま使えます。
-- ここでは `Tonatona.Logger` の機能を使ってログ出力を行っています。
lift $
TonaLogger.logInfo $ display $
("This log is called inside of `TonaDb.run`" :: Text)
-- `persistent` に定義されている `insert_` 関数です。
insert_ $ BlogPost "Mr. Foo Bar" "This is an example blog post"
TonaLogger.logInfo $ display ("Successfully inserted a blog post!" :: Text)
完成したコードの全体像
最終的にこのようなコードになりました。
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
module TonaApp.Main where
import RIO
import Database.Persist (insert_)
import Database.Persist.TH (mkMigrate, mkPersist, persistLowerCase, share, sqlSettings)
import Tonatona (HasConfig(..), HasParser(..))
import qualified Tonatona.Logger as TonaLogger
import qualified Tonatona.Persist.Sqlite as TonaDb
-- DB entity defs
$(share
[mkPersist sqlSettings, mkMigrate "migrateAll"]
[persistLowerCase|
BlogPost
authorName Text
contents Text
deriving Show
|]
)
-- App
app :: RIO Config ()
app = do
TonaLogger.logInfo $ display ("This is a skeleton for tonatona project" :: Text)
TonaLogger.logDebug $ display ("Migrating DB..." :: Text)
-- DBにテーブルを作成
TonaDb.runMigrate migrateAll
TonaLogger.logDebug $ display ("Running DB query..." :: Text)
-- `TonaDb.run` の中で `persistent` のDB操作関数が使えます
TonaDb.run $ do
-- `TonaDb.run` の中でも、`lift` を使うことで他のプラグインの機能をそのまま使えます。
-- ここでは `Tonatona.Logger` の機能を使ってログ出力を行っています。
lift $
TonaLogger.logInfo $ display $
("This log is called inside of `TonaDb.run`" :: Text)
-- `persistent` に定義されている `insert_` 関数です。
insert_ $ BlogPost "Mr. Foo Bar" "This is an example blog post"
TonaLogger.logInfo $ display ("Successfully inserted a blog post!" :: Text)
-- Config
data Config = Config
{ tonaLogger :: TonaLogger.Config
, tonaDb :: TonaDb.Config
}
instance HasConfig Config TonaLogger.Config where
config = tonaLogger
instance HasConfig Config TonaDb.Config where
config = tonaDb
instance HasParser Config where
parser = Config
<$> parser
<*> parser
実行してみる
では、これをコンパイルしてヘルプを見てみましょう。
$ stack install --pedantic
$ stack exec sample-app -- -h
Application deployment mode to run
Default: Development
Type: DeployMode (Development|Production|Staging|Test)
Command line option: --env
Environment variable: ENV
Make the operation more talkative
Default: False
Type: Bool (False|True)
Command line option: --verbose
Environment variable: VERBOSE
Formatted string to connect postgreSQL
Default: :memory:
Type: String
Command line option: --db-conn-string
Environment variable: DB_CONN_STRING
Number of connections which connection pool uses
Default: 10
Type: Int
Command line option: --db-conn-num
Environment variable: DB_CONN_NUM
Display this help and exit
Default: False
Type: Bool
Command line option: -h
Command line option: --help
どのDBにつなぐかとか、コネクションプールのコネクション数をどうするかなど、環境変数やコマンドラインオプションで指定できるようになっています。
デフォルト値で実行すると以下のようにDBアクセスできていそうなことが確認できます。
$ stack exec sample-app
2018-12-02 18:45:01.605876: [info] This is a skeleton for tonatona project
@(src/TonaApp/Main.hs:36:3)
2018-12-02 18:45:01.606421: [debug] Migrating DB...
@(src/TonaApp/Main.hs:37:3)
Migrating: CREATE TABLE "blog_post"("id" INTEGER PRIMARY KEY,"author_name" VARCHAR NOT NULL,"contents" VARCHAR NOT NULL)
[Debug#SQL] CREATE TABLE "blog_post"("id" INTEGER PRIMARY KEY,"author_name" VARCHAR NOT NULL,"contents" VARCHAR NOT NULL); []
2018-12-02 18:45:01.608972: [debug] Running DB query...
@(src/TonaApp/Main.hs:40:3)
2018-12-02 18:45:01.609197: [info] This log is called inside of `TonaDb.run`
@(src/TonaApp/Main.hs:47:7)
[Debug#SQL] INSERT INTO "blog_post"("author_name","contents") VALUES(?,?); [PersistText "Mr. Foo Bar",PersistText "This is an example blog post"]
[Debug#SQL] SELECT "id" FROM "blog_post" WHERE _ROWID_=last_insert_rowid(); []
2018-12-02 18:45:01.609775: [info] Successfully inserted a blog post!
@(src/TonaApp/Main.hs:52:3)
プラグイン紹介
Tonatona のプラグインは現在以下のものが提供されています。
(stack new
でテンプレートから作成する場合にはすぐに使えるようになっています)
新しいプラグインの作成も歓迎しますので、「persisntent よりも HRRだろ!」って方はぜひご協力ください。
-
RIO のロギング関数が設定なしに使えるようになります。
-
tonatona-persistent-postgresql
persistent を使った PostgreSQL への接続をサポートします。
-
persistent を使った SQLite への接続をサポートします。
-
servant server を設定なしに立ち上げられます。
まとめ
トナカイのコスプレをしたさくらちゃんがかわいいメタアプリケーションフレームワークである Tonatona をご紹介しました。
プラグインアーキテクチャの威力と、考え抜かれてむしろ平凡に感じるような設計思想によって
「Tonatona を使えば新規開発も高速にでき、保守性も高い」と言われる未来の一端を感じていただけたなら幸いです。