Help us understand the problem. What is going on with this article?

チュートリアル:Haskell の Snap と PostgreSQL Simple Snaplet を使ったシンプルな Web アプリケーションの作り方

More than 5 years have passed since last update.

この記事は Tutorial: Building a Sample Application with Haskell Snap, PostgreSQL, and the PostgreSQL Simple Snaplet - Janrain の翻訳である。


2013年4月25日 LUC PERKINS 著

このドキュメントを読んでいるあなたは、もしかしたら Haskell もしくは Postgres に夢中になる必要がないかもしれない。なので、単刀直入に本題に入る。なぜそれら2つが非常に驚くべきものなのか、Janrain では Janrain CaptureHaskellSnap ウェブフレームワークと PostgreSQL (同様に PostgreSQL Simple それ自体)を使うので、それらについて確信を持って言うことができるが、それらを伝える代わりに、すぐにチュートリアルに入ろう。

このチュートリアルは、100%新しいチュートリアルというより、たくさんの情報源(これこれなど)を1つに繋げたプレゼンテーションである。しかし、私は、これが既存のたくさんの情報を分かりやすい手順書の形式にするのに役立つことを願っている。このチュートリアルでは、とても単純な製品管理ツールを作成する。基本的に必要最低限で有益な Basecamp クローンである。UI に関しては何もせず、サーバーとのやりとりは cURL で行なう。見た目や装飾的な美しさについては別の日のためにとっておく。

Snap アプリケーションを立ち上げる

初心者のために Snap の初歩的な手順を提供するので、手短に Snap アプリケーションを新規作成する方法を示す。HaskellcabalSnap がインストールされているならば、単に新しいプロジェクトディレクトリーを作成し(以降、これから作るプロジェクトを projectomatic と呼ぶ)、そのディレクトリーに移動し、新規 Snap アプリケーションを開始する。

mkdir projectomatic
cd projectomatic
snap init

完了すれば、llls もしくは何かそういったもので、何種類かのファイルやディレクトリーが作成されたことが分かる。ここではそれらについて深くは追求しない。もし Snap アプリケーションの構成についてもっと知りたいならば、ここ を読んで調べることを薦める。snap init barebones を実行するとより無駄なものを取り除いたアプリケーションを作成することができるが、Snaplets を早く使えるようになるように、ここではフルインストールを選ぶ。

PostgreSQL simple を使うためにアプリケーションの初期設定をする

Snaplets の説明をするのは少し難しいが、簡単に要約してみる。Snaplets は Snap アプリケーションの中から外の世界への玄関のようなものである。Snaplets を使うことで、状態(例えば、データベーストランザクションがちょうどそれに当たる)にアクセスすることが、比較的痛みなくできる。PostgreSQL Simple を使うようにアプリケーションを設定するには、とてもお手軽な PostgreSQL Simple Snaplet を使う。まず、cabal でインストールし(cabal install snaplet-postgresql-simple)、projectomatic.cabal ファイルの依存にそれを加え、cabal install し、src/Application.hs ファイルを下記のように修正する。

src/Application.hs
{-# LANGUAGE FlexibleInstances #-}

import Control.Lens
import Snap (get)
import Snap.Snaplet
import Snap.Snaplet.PostgresqlSimple

data App = App
    { _pg :: Snaplet Postgres }

makeLenses ''App

instance HasPostgres (Handler b App) where
    getPostgresState = with pg get

普通の snap init でインストールすると、他に何も要らないようにいくつかの Snaplets を使うように設定される。自由にそれらを残しておいてよい。簡潔さの点でこのチュートリアルではそれらは削除する。上記のコードがしていることは、App 型に Postgres Snaplet を組み込み、makeLenses ''App 関数で pg というアクセサーを生成している。最後に、Postgres 関連の状態にアクセスできるように HasPostgres (Handler b App) のインスタンスを定義している。

pg アクセサーが用意できたので、src/Site.hs ファイルを開き、追加の設定をする。まず、インポートに PostgreSQL Snaplet を追加する。

src/Site.hs
import Snap.Snaplet.PostgresqlSimple

pg コンストラクターをアプリケーションのコアに追加しなくてはならない。Postgres Snaplet を含めるために app 関数の定義を修正しよう。

src/Site.hs
app :: SnapletInit App App
app = makeSnaplet "app" "My stunningly advanced Snap application." Nothing $ do
    pg <- nestSnaplet "pg" pg pgsInit
    addRoutes routes
    return $ App pg

これでほとんど準備はできた。残ったのは Postgres との接続である。snaplet-postgresql-simple モジュールを依存に追加した後に cabal install すると、Snap は snaplets/postgresql-simple ディレクトリーを自身のディレクトリー下に作る。(これってすごくない?)そのディレクトリーには、データベース接続の設定が書かれた devel.cfg ファイルがある。私のは下記のようになっている。(もちろんあなた自身のものに変更してね!)

devel.cfg
host = "localhost"
port = 5432
user = "luc"
pass = ""
db = "projectomatic"

他にも設定項目はあるが次に進むにはこれで十分である。これができたなら、cabal install を実行しコンパイラーエラーがないかチェックする。もしあれば自身でデバッグして、なければ次の章に進もう。

このプロジェクトの型

Haskell は本当に型についてうるさいので、これからやる全てについて、明白にかつ注意深くならなくてはいけない。src/Site.hs ファイルに Project 型を定義しよう。別ファイルにそれをすることもできるが、簡単のためにそうはしないことにする。Text 型を使うので Data.Text をインポートに追加する。(文字列の使用は Haskell では非推奨だと広く考えられている。)

src/Site.hs
import qualified Data.Text as T

型をシンプルにしておく。

src/Site.hs
data Project = Project
  { title       :: T.Text
  , description :: T.Text
  }

作った型を扱うように Postgres の初期設定をする

Postgres は(少し追加されたところはあるが)伝統的な列指向のデータベースである。なので、単にキー・バリュー・ペアを投げると、それが保存されるというわけではない。束縛するための準備のできたテーブルを作成する必要がある。あ、データベースもね。それから始めよう。*nix 環境では、コマンドラインから新規にデータベースを作成する機会はほぼ確実にある。(すでに Postgres がインストールされていると仮定すると。)

createdb projectomatic

もし失敗するなら、たくさんある Postgres インストールチュートリアルを参考にするか、本当に簡単に使える Postgres.app をダウンロードして本題に入るかするとよい。もし成功したなら、projectomatic.sql ファイルに下記 SQL を書いて、それをプロジェクトの主ディレクトリーに置こう。

projectomatic.sql
CREATE TABLE projects (
  title TEXT NOT NULL,
  description TEXT NOT NULL,
);

どのプロジェクトにも、整数の識別子と題名、備考、関わっているユーザーの集合がある。簡単にするために、それぞれのユーザーにはユーザー名、関わっているプロジェクトの集合がある、ただしどの項目も NULL にはならない、というように作る。

コマンドラインで psql projectomatic でデータベースを開く。そして、下記のように SQL スクリプトを実行する。

i /path/to/application/dir/projectomatic.sql

レスポンスとして CREATE TABELE が返ってきたならば、次に行く準備がおそらくできた。dt を実行して、 projectuser テーブルが存在しておそらく構築されているということを確認する二重チェックはいい考えである。これができたら、Postgres そのもに入っていく準備ができている。

作った型を Postgres で使えるようにする

一度言及したように、Haskell では型は 厳格 である。どんなプログラミング言語でも型を扱うことは「簡単」であると、本当の意味で主張したことは一度もないが、Haskell ではかなり手強い学習曲線を示す。幸運なことに、本当に基本的なことをしている場合は、それはそんなに悪いものではない。

事前に、Project 型について明記したが、Postgres と実際に対話するにはまだ十分ではない。Posgres データを書き込むのは、この段階でだいたいうまくいくだろう。しかし、Postgres から データを読み込んで、アプリケーションから Haskell で使える形に変換するにはまだいくらかすることがある。

Site.hs ファイルで、作った主な2つの型を FromRow のインスタンスにする必要がある。FromRow という名の PostgresSQL Simple の特別なサブモジュールだけでなく Control.Applicative も必要である。Site.hs ファイルに追加する必要があるのは下記である。

src/Site.hs
import Control.Applicative
import Database.PostgreSQL.Simple.FromRow

instance FromRow Project where
    fromRow = Project <$> field <*> field

奇妙な <$> とか <*> といった演算子は Control.Applocative からきている。一番大切なことは、インスタンスを作るときの field の個数と、データベースのフィールドの個数が一致していることである。さもないと、例えば varchar もしくは text しかとらないフィールドに整数値を書き込もうとしたときなど、ちょうど型の不一致のときのような、あらゆる種類の分かりづらいコンパイラーエラーが発生するだろう。

これら FromRow インスタンスのコンストラクターのおかげで、Postgres からデータを取り出すことができるようになる。例えば、SELECT * FROM projects のような形のクエリーの結果のように。そして、それらは純粋な Haskell アプリケーションから呼び出すことができる。もう1つすべきことは、作った型を Show のインスタンスにすることだ。なぜなら、プロジェクトやそのリストを見るために HTTP リクエストを生成したとき、コマンドラインでそれらがどう見えるか特定しておきたいからである。もし Show のインスタンスにしていなければ、例え他が全て正しくても、何も実際に見ることはできない。基本を示すとこうだ。

src/Site.hs
instance Show Project where
    show (Project title description) =
      "Project { title: " ++ T.unpack title ++ ", description: " ++ T.unpack description ++ " }n"

テキストを文字列に変換するために、Data.Text モジュールの T.unpack 関数を使う必要がある。そうしないと、コンパイラーエラーが出る。

作っていたアプリケーションに戻るときが来た

たくさんの初期設定だった。それに関しては疑いがない。さて、実際に HTTP リクエストを PostgreSQL クエリーとその結果としてのデータに変換するときがきた。下記のように、リクエストを受け取るサーバーを設定する。

  1. 新規プロジェクトを作成する
  2. 全プロジェクトのリストを返す
  3. プロジェクトを削除する

基本的に Project 型についての単純な CRUD 操作を作る。

新しいプロジェクトを作成しよう。まずルーティグに関して。

src/Site.hs
("/project/new", method POST createNewProject)

次に対応する関数。

src/Site.hs
createNewProject :: Handler App App ()
createNewProject = do
  title <- getPostParam "title"
  description <- getPostParam "description"
  newProject <- execute "INSERT INTO projects VALUES (?, ?)" (title, description)
  redirect "/"

ちょっと解説をしよう。まず、do ブロックの中に埋め込まれてある。do ブロックは、だいたいの場合、入出力を扱っていることを表している。いつもそうというわけではないが。この関数が最初にしているのは、サーバーに渡ってきたフォームのデータの中から、getPostParam 関数を使って、新しいプロジェクトの title を取り出している。decription についても同様である。ここで、execute 関数を使って Postgres と対話する。後に出てくる query_ 関数と比較して、execute は一般的に返り値を期待しない箇所で使われる。例えばテーブルに書き込むときのような。

2つの ? は値を挿入する SQL バイト文字列の場所である。バイト文字列のすぐ後に、どの値を挿入するか指定する。ここでは、URL に由来する titledescription である。

さて、execute 関数の代わりに query_ を使おう。これは少し技巧的である。なぜなら、execute 関数では、すでに整形されたデータを Postgres に渡すのに対して、query_ では Postgres からデータを取り出し、アプリケーション側で処理する必要があるからである。全てのプロジェクトを見せるようにルーティングを設定しよう。

src/Site.hs
("/projects", method GET getAllProjects)

そして、処理する関数。

src/Site.hs
getAllProjects :: Handler App App ()
getAllProjects = do
  allProjects <- query_ "SELECT * FROM projects"
  liftIO $ print (allProjects :: [Project])

ここでは少しだけ言及する。まず、allProjects 変数にクエリーの結果を保存する。それはとても単純である。しかし、もっと難しいのは、それらのプロジェクトを 見せる ことである。最初に print 関数に着目しよう。何が表示されるか。allPorjects 変数が、プロジェクトのリストとして表示される。しかし、ptint は入出力を伴うのでそれ単体では使えない。入出力は Haskell では常に技巧的である。

ここが liftIO 関数が役に立つところである。この関数によって、アプリケーションが関連する入出力を扱うことができる。この場合では、IO () 型を Handler App App () 型に変換する。アプリケーションの文脈へ持ってこられた入出力を print は生成する。ビューや、データを綺麗に表示する何かを用意していないので、print 関数は GET リクエストの結果をコンソールに表示する。

プロジェクトを作ったり全てのプロジェクトを取得したりできるようになった。ルーティングと、指定したプロジェクトを削除する関数を追加しよう。まず、下記のようにプロジェクトの題名をもとにプロジェクトを削除するルーティングを設定する。

src/Site.hs
("/project", method DELETE deleteProjectByTitle)

慎重なプロジェクトでは、整数値の ID や何かそういったものをもとに削除したいかもしれないが、ここではシンプルに題名にする。対応する関数は下記の通りだ。

src/Site.hs
deleteProjectByTitle :: Handler App App ()
deleteProjectByTitle = do
  title <- getPostParam "title"
  deleteProject <- execute "DELETE FROM projects WHERE title = ?" (Only title)
  redirect "/"

ここでは、SQL bytestring には1つの値を渡しているので、Only インスタンスである。プロジェクトの題名は URL 経由ではなく、むしろフォームデータ経由である。

Postgres と対話したり、いくつかの本当に基本的なことをしたりするルーティングが用意できた。作ったサーバーをテストにかけよう。

勝利への道を cURL する

前に言及したように、ビューや HTML、ほかそういったたぐいのものについては述べない。今のところ、作っているアプリケーションのビューの数は0である。Snap のデフォルトのテンプレートシステムである Heist については将来のチュートリアルで行う。今は古い形式、cURL で作ったサーバーに接続する。cURL についての私のお気に入りのチュートリアルはこれである。

これまでに作成した各々のルーティングに関連する例に進もう。まず、サーバーを起動する。

cabal install
projectomatic -p 3000

では、全プロジェクトのリストを取得しよう。

curl -X GET http://localhost:3000/projects

最初、これは空のリストを返すはずだ。それに驚きはない。では、プロジェクトを作ろう。“Getting in shape” という名前で “Daily exercise” という説明文のプロジェクトを作成しよう。

curl -X POST http://localhost:3000/project/new
-d 'title=Getting in shape'
-d 'description=Daily exercise'

/project に対して GET リクエストを投げれば、端末に下記が得られるだろう。

[Project { title: Getting in shape, description: Daily exercise}
]

これは作ったサーバーがちゃんと動いてるってことだ。やったね。

次は、さっきデータベースに追加したプロジェクトを削除しよう。title は form データを経由したのに対して、URL が /project の形をしていたのを思い出す。cURL リクエストは下記のようになるだろう。

curl -X DELETE http://localhost:3000/project
-d 'title=Getting in shape'

データベースに “Get in shape” というタイトルのプロジェクトがあった場合、それらは全て消される。これは、大切な型に対して整数の ID をふることは、このような属性を使うよりもよい、もう1つの理由である。今後のチュートリアルでは、より正確に詳しく Snap と Postgres を見ていこう。

基本的な入門

前に示したように、このアプリケーションはビューやエラー処理、認証がない。単なる Postgres とお話をする HTTP サーバーである。また、PostgreSQL Simple Snaplet の売りの最小限にしか言及していない。しかし、Haskell と Postgres でウェブ開発をすることに対するあなたのおそれが、最初よりは軽減できたことを祈っている。少なくとも私のようなオブジェクト指向人間にとっては概念的飛躍があるが、忍耐は報われる。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away