F# でブログアプリを作る (Very Easy 編)

このエントリは F# Advent Calendar jp 2010 の第四回のものです。
第二回で、

既存の C# で書かれた ASP.NET Web アプリのどこかのサーバー側ロジックを、F# で記述することは容易なはずだ。


だからこそ、単なるお勉強言語としてではなく、実際にお客様にお届けする ASP/SaaS の奥底で、実は F# で書かれたコードが淡々と動作している、という未来を想像し期待しているのだ。


もちろんそこには、「C# でも VB でもいいんだけど、F# つかうと、こんなメリットが!」という背景があってこそである。
(ユーザーにしてみれば、実装が C# でも VB でも F# でも Java でも PHP でも Ruby でも、そのこと自体は直接には関係ない。ただ、生産性による短納期や、バグ等の品質として、ユーザー様へのメリットにつながってくる)


ただ非常に残念なことに、私自身は、いまだ F# を使うことの生産性や保守性などの観点からの利点を体感できる学習レベルに至っていない。
また、あくまでも私自身の個人的経験においてだが、ズバリ、「こういうコードが F# はすごいでしょ」みたいな話や事例、メッセージを見聞きできていない。


なので、どなたか、「こういうコードを F# で書くと、ほら、簡単・早い・速い・簡潔・クールでナイス!」みたいな情報を発信して頂けるとうれしいのにな、などと自分勝手に夢想する次第である。


言うなれば「15分でブログサイトを構築できる、Ruby on Railsってすごいや!」の F# 版エピソードが欲しい・知りたいわけだ。

ASP.NET は F# の夢を見るか? - F# Advent Calendar jp 2010 -

というエントリが上がっていたので、あー、これは ASP.NET MVC で F# やったことあるしエントリにする流れ!ということで、F# でごくごくシンプルなブログアプリを作ってみることにしましょう。
ただし、「15 分で〜」とか、「ここがすごい〜」とかっていう話はないです。それはまた機会があるときにでも、ということでお願いします。

完成品はこちらにあります

http://cid-c562dfdeb23518f0.office.live.com/self.aspx/.Public/MyBlog.zip

実際にコードをみながらこのエントリの内容を見たほうがわかりやすいかも、ということで。

つくるもの

簡単なブログアプリを作ります。
できることとして、

  • 全エントリをトップページに表示
  • エントリのタイトルのリンクから、エントリ詳細にジャンプ
  • エントリ詳細ページでコメントが見れる
  • エントリ詳細ページからコメントを投稿できる
  • エントリを書く

色々としょぼいですね。でもまぁ最初と言うことで。

準備

まず、VS2010 と F# PowerPack と SQL Server 2008 を開発環境とします。VS2010 で F# の開発環境構築がまだの方はそこから行ってください。
開発環境が整ったら、楽をするためにプロジェクトのテンプレートを導入しましょう。
Tomas Petricek さんのエントリの一番下にある「Download Visual Studio 2010 project template」というリンクをクリックしてダウンロードして、実行してください。
これで、VS2010 でソリューションを新しく作る際に「F# MVC Web Application」が選べるようになります。

作成開始!

プロジェクト名を「MyBlog」とでもして、プロジェクトを作成しましょう。
すると、DB アクセスも行う簡単なサンプルアプリケーションが作成されます。
C# による ASP.NET MVC アプリケーションとの違いは、

  • 3 つのプロジェクトからなる
  • Web アプリケーションのプロジェクトに、Controllers や Models といったフォルダがない
  • Global.asax に対応する .cs ファイルがない

といったところでしょうか。

構成

このテンプレートによって生成される構成を少し説明します。
このテンプレートでは、すべてを F# で作成するわけではなく、ルーティングの制御とモデル、コントローラーを F# で、それ以外 (主にビュー) を C# で記述することになります。
そのため、WebApplication プロジェクト (C#) と WebApplication.Core プロジェクト (F#) に分かれています。


さらに、DB アクセス部分も C# で作成することになります。
デフォルトでは LINQ to SQL を使ったプロジェクト (WebApplication.Data) が作成されますが、Entity Framework など、ほかの技術を使用することも可能です。
DB から生成する場合、ここで C# のコードを書くことはほとんどないでしょう。


さて、肝心の WebApplication.Core プロジェクトですが、初期状態では、

  • Model.fs
  • HomeController.fs
  • ProductsController.fs
  • Global.fs

の 4 ファイルが含まれています。
ASP.NET MVC を触ったことがある人なら、それぞれ何を表しているのかは想像がつくでしょう。
Model.fs はそのまま、モデルを表す型を格納する場所です。
HomeController.fs や ProductsController.fs はコントローラを表し、ルーティング情報によって呼び出されるコントローラとそのメソッド (アクション) が決定されます。
例えば、デフォルト状態で http://localhost/Home/Index にアクセスした場合、HomeController の Index メソッドが呼び出されます。
Global.fs は、そのルーティングをカスタマイズする際に弄る場所です。今回はデフォルト状態のまま変更しませんが、一度のぞいておくといいでしょう。

いらないものを削除

生成されたファイルを眺めるのもいいですが、とりあえず今回はさっさと簡単なブログアプリケーションを作っちゃいましょう。
そのために、不要なファイルをバンバン削除します。
いらないファイルは、

  • WebApplication
    • App_Data
      • NORTHWND.MDF
    • Views
      • Home
        • About.aspx ←
      • Products ←
        • Detail.aspx ←
        • List.aspx ←
  • WebApplication.Core
    • ProductsController ←
  • WebApplication.Data

です。

ファイルの編集

Model.fs の中に、さっき消したファイルの内容に依存している部分があるので、そこを消しちゃいましょう。
とりあえず、

namespace WebApplication.Core

open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Linq
open Microsoft.FSharp.Linq.Query

open WebApplication.Data

module Model =
  let Dummy = -1

とでもしておけばいいでしょう。
これでコンパイルは通るようになり、一応実行もできるようになります。

DB を作る

DB とか今回脇役なので、具体的な説明はしません。いい感じのテーブルさえあれば OK です。
データベース名は「MyBlogDatabase」で、テーブル作成の SQL は、

CREATE TABLE [dbo].[Entries](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [Title] [nvarchar](140) NOT NULL,
    [Body] [nvarchar](max) NOT NULL,
    [Created] [datetime] NOT NULL,
 CONSTRAINT [PK_Entries] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

CREATE TABLE [dbo].[Comments](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [EntryId] [int] NOT NULL,
    [Writer] [nvarchar](50) NOT NULL,
    [Body] [nvarchar](max) NOT NULL,
    [Created] [datetime] NOT NULL,
 CONSTRAINT [PK_Comments] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

ALTER TABLE [dbo].[Comments]  WITH CHECK ADD  CONSTRAINT [FK_Comments_Entries] FOREIGN KEY([EntryId])
REFERENCES [dbo].[Entries] ([Id])

ALTER TABLE [dbo].[Comments] CHECK CONSTRAINT [FK_Comments_Entries]

です *1


SQL 文を実行したら、VS のサーバー エクスプローラーを開いて、データ接続に新しい接続を追加しておきましょう。

WebApplication.Data を完成させる

DB ができたので、こいつを操作するクラスを作りましょう。
ここではテンプレートが使っていた LINQ to SQL を使います。
WebApplication.Data を右クリックして、新しい項目を追加しましょう。

MyBlog.dbml という名前で追加しました。


追加したファイルが開くので、サーバー エクスプローラーから先ほどのテーブル 2 つを選択して、左側の領域にドラッグアンドドロップします。

これで WebApplication.Data は完成です。保存してビルドしておきましょう。

モデルを作る

次にモデルを作ります。
まず、ビューとやり取りする型を作る必要があるので、そこから行きましょう。

// ...略
open WebApplication.Data
open System

type EntryDetail = {
  Id : int
  Title : string
  Body : string
  Created : DateTime
  Comments : Comment seq
} and Comment = {
  Writer : string
  Body : string
  Created : DateTime
}

type EntrySummary = {
  Id : int
  Title : string
  Body : string
  Created : DateTime
}

module Model =
  let Dummy = -1

EntrySummary はトップ画面用、EntryDetail は詳細画面用です。


今回は単純なアプリケーションなので、これらの型に対する操作も作ってしまいましょう。
必要なのは、

  • seq EntrySummary の取得
  • id による EntryDetail の取得
  • エントリの投稿
  • コメントの投稿

です。

module Model =
  let ListEntries =
    let dx = new MyBlogDataContext()
    <@ seq { for e in dx.Entries |> Seq.sortBy (fun e -> -e.Id) do
               yield { Id = e.Id
                       Title = e.Title
                       Body = e.Body
                       Created = e.Created } } @> |> query
  
  let Entry id =
    let dx = new MyBlogDataContext()
    let entry =
      <@ seq { for e in dx.Entries do
                 if e.Id = id then
                   yield { Id = id
                           Title = e.Title
                           Body = e.Body
                           Created = e.Created } } |> Seq.head @> |> query
    let comments =
      <@ seq { for c in dx.Comments |> Seq.sortBy (fun c -> c.Id) do
                 if c.EntryId = id then
                   yield { Writer = c.Writer
                           Body = c.Body
                           Created = c.Created } } @> |> query
    { Id = id
      Title = entry.Title
      Body = entry.Body
      Created = entry.Created
      Comments = comments }
  
  let PostEntry title body =
    let dx = new MyBlogDataContext()
    let e = new Entries()
    e.Title <- title
    e.Body <- body
    e.Created <- DateTime.Now
    e |> dx.Entries.InsertOnSubmit
    dx.SubmitChanges()
  
  let PostComment id writer comment =
    let dx = new MyBlogDataContext()
    let c = new Comments()
    c.EntryId <- id
    c.Writer <- writer
    c.Body <- comment
    c.Created <- DateTime.Now
    c |> dx.Comments.InsertOnSubmit
    dx.SubmitChanges()

本当は日付を降順で並び替えたかったんですけど、どう書けばちゃんと LINQ to SQL に変換されるのかわからなかったので、-e.Id して並び替えています。


ここまで終わったら、ビルドしておきましょう。

ビューを作る その 1

モデルを作ったので次はビューです。最初に Home/Index.aspx は消さなかったのですが、あれは動く状態にしておきたかっただけなので、まずはこいつを削除しちゃいましょう。


削除したら、Home を右クリックして、追加から「ビュー」を選択します。
ビューの名前を「Index」にして、「厳密に〜」にチェックを入れます。
ビューデータクラスには「WebApplication.Core.EntrySummary」を選択し、ビューコンテンツに「List」を選択します。

Content2 の中をごっそり消して、

<% foreach (var item in Model) { %>
<div class="entry">
    <h2><%: Html.ActionLink(item.Title, "Detail", new { controller = "Entry", id = item.Id }) %></h2>
    <div class="body">
        <%: item.Body %>
    </div>
    <div class="created"><%: item.Created.ToString() %></div>
</div>

<% } %>

こう書き換えます。

ビューとモデルを関連付ける

ビューを作ったら、アクションが呼び出された際にビューとモデルを関連付けてあげる必要があります。
HomeController.fs を開き、内容を

namespace WebApplication.Core.Controllers

open System
open System.Web.Mvc
open System.Reflection

open WebApplication.Core

[<HandleError>]
type HomeController() =
  inherit Controller()

  member x.Index() =    
    x.View(Model.ListEntries)

こう書き換えます。
Home コントローラに対して Index アクションが呼び出されると、ビューに対して全エントリが渡されます。


これで、トップページを開くと全エントリが表示されるようになりました。
・・・が、最初はエントリが全くないので、次はブログエントリの投稿ができるようにしましょう。

Site.Master を弄る

エントリの投稿は WebApplication/Views/Shared/Site.Master にリンクを作って飛ばしましょう。
Site.Master の、

<%: Html.ActionLink("Products list", "List", "Products")%>
<%: Html.ActionLink("About", "About", "Home")%>

の部分を、

<%: Html.ActionLink("エントリを書く", "Create", "Entry")%>

に変更します。
これで、どこからでもエントリを書くページに飛ぶことができるようになりました。

コントローラを作る

エントリを書くページへのリンクは作りましたが、その場所がまだありませんので作りましょう。
Html.ActionLink の引数は先頭から、「表示するテキスト」、「アクション名」、「コントローラ名」となっていますので、Entry コントローラの Create アクションと、対応するビューを作る必要があります。


まずは Entry コントローラを作りましょう。WebApplication.Core を右クリックして、追加から新しい項目の追加を選択し、EntryController.fs を作成します。

namespace WebApplication.Core.Controllers

open System
open System.Web.Mvc
open System.Reflection

open WebApplication.Core

[<HandleError>]
type EntryController() =
  inherit Controller()

  [<HttpGet>]
  member x.Create () =
    x.View()

とりあえずここまで。

ビューを作る その 2

そしてビューを作ります。WebApplication の Views に Entry フォルダを作り、その中にビューを作ります。

ビューの中身はこんな感じ。

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<dynamic>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    エントリを書く
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <% Html.BeginForm(); %>
    <input name="title" type="text" /><br />
    <textarea name="body" rows="40" cols="80"></textarea><br />
    <input type="submit" />
    <% Html.EndForm(); %>

</asp:Content>

さて、これだけだと Submit ボタンを押したときに対応するアクションがなくてエラーになってしまうので、コントローラ側も弄りましょう。

[<HttpPost>]
member x.Create title body =
  Model.PostEntry title body
  x.RedirectToAction("Index", "Home")

を EntryController に追加します。
これで、エントリを投稿して記事の一覧を見ることができるようになりました!
実行して確認してみてください。

以下略

あとは同じような流れなので割愛します。
コードとしては、Detail ビューが

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<WebApplication.Core.EntryDetail>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Detail
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2><%: Model.Title %></h2>
    <div class="body">
        <%: Model.Body %>
    </div>
    <div class="created"><%: Model.Created.ToString() %></div>
    <hr />
    <% foreach (var c in Model.Comments) { %>
    <div class="comment">
        <div class="comment-writer"><%: c.Writer %></div>
        <div class="comment-date"><%: c.Created.ToString() %></div>
        <div class="comment-body"><%: c.Body %></div>
    </div>
    <% } %>
    <% Html.BeginForm(new { id = Model.Id }); %>
    <input name="writer" type="text" /><br />
    <textarea name="comment" rows="5" cols="50"></textarea><br />
    <input type="submit" />
    <% Html.EndForm(); %>

</asp:Content>

で、それに対するアクション

[<HttpGet>]
member x.Detail id =
  x.View(Model.Entry id)

[<HttpPost>]
member x.Detail id writer comment =
  Model.PostComment id writer comment
  x.View(Model.Entry id)

を追加したくらいです。
これで、目標にしていた機能は達成することができました。


ただ、

  • 誰でも投稿できるこれは果たしてブログと呼べるのか?
  • トップページに全エントリ表示とか凶悪すぎる
  • 改行とかむしするし色々なんかひどい
  • プレーンテキストのみ?

と、色々改善すべきところはありますが・・・

さて

なんか F# のエントリじゃなくて ASP.NET MVC のエントリっぽくなってますね。
それは置いといて、F# で ASP.NET MVC も構築できなくはない、というのを感じてもらえたでしょうか?
ただし、現状では「できなくはない」レベルで、口が裂けても「生産性高い!」だとか、「簡単!」だとかは言えません。
そのため、まだ F# で ASP.NET 系の開発をやるのは難しいとは思います。
が、全部を F# でやる必要はないのです。
WebApplication は WebApplication.Core を使っていましたよね?つまり、C# から F# を呼び出していました。
WebApplication.Core は WebApplication.Data を使っていましたよね?つまり、F# から C# を呼び出していたのです。
このように、F# は他の .NET 言語との相性が非常にいい。
これを考えると、F# を使うといいところというのも見えてくると思います。
F# は C#VB に比べ、型を作るのが非常に楽ですし、パターンマッチも使えます。もちろん関数は first class object です。ですので、関数型言語が得意な分野を F# に任せ、それを C#VB から呼び出せばいいのです。F# に特化した高レベルなライブラリはまだ少ないですが、C#VB で書かれたものがあるなら、それを F# から使えばいいのです。


例えば、最近仕事で NParsec を使って DSL 的なものを解析して実行とかやっているんですが、正直、C# の文法でパーサコンビネータは厳しいです。
その点、F# で書かれた FParsec は NParsec よりも非常に分かりやすく、短いコードで同じことが実現できます。
NParsec を使った仕事は .NET Framework 3.5 だったので F# を使わなかったんですが、4.0 ならまず間違いなく F# を提案したでしょう。


ということで、あんまり F# のいいところとか紹介できませんでしたが、それはこの後に続くすごい人たちがどうにかしてくれるでしょう。
次はどんな勉強会でも Coq の話に持っていくことで有名な、id:mzp (みずぴーさん) です。やばいですね。

*1:デザイナで作って吐いた SQL なので汚いけど気にしない