- なぜ Haskell で Web Serivce を書くのか -
-
-
-
- RubyやPythonなどのインタプリタ言語と比較して高速です -
- 高水準言語で、CやC++、Javaよりも自分の足を撃ち抜きにくいです -
# HG changeset patch
# User Daichi TOMA
- 純粋とは、一度変数の値を設定すると、変更することは出来ないということです。 + 純粋なので、一度変数の値を設定すると、変更することは出来ません。
関数型言語では、引数に関数を作用させていくことで計算を行います。
- - --
- 命令型プログラミング言語では、命令の並びをコンピュータに与えて、それを実行します。 -
-- 関数型プログラミング言語では、何であるかという定義を与えます。 -
-- 例:n番目のフィボナッチ数を求める関数の定義 -
-fib 0 = 0 -fib 1 = 1 -fib n = fib (n - 1) + fib (n - 2) -+ 他にも遅延評価や、強い静的型付けなどの特徴があります。
-
- Haskell は副作用を持ちません。 -
-- つまり、変数を書き換えることはできないということです。 -
-- 関数は同じ引数で呼ばれた場合、同じ値を返すことを保証します。 -
-- 関数の正しさを簡単に推察でき、正しいと分かっている関数同士を容易に組み合わせることができます。 -
- - --
- Haskellは、結果が必要になるまで関数を評価しません。 -
-- 遅延評価のため、無限の大きさのデータを扱うことが可能です。 -
-- 例:奇数の無限のリストから最初の3つを入手する -
-take 3 [1,3..] -- -
-
- 型規則に従ってない式が存在しないことを保証します。 + 強い静的型付けにより型規則に従ってない式が存在しないことを保証します。
また型推論を持つため、すべての式に明示的に型を書く必要はありません。
+ しかしながら、実際にはどのような関数なのか表すために明示的に型を書くほうが良いです。 +
+コンパイルが通れば概ね思い通りに動くのもHaskellの特徴です。
一番手っ取り早い方法は、Haskell Platformを導入することです。
@@ -172,7 +97,7 @@
Terminal を開き、
@@ -193,9 +118,9 @@
- Haskell Platform をインストールしたら
+ GHCiの起動
- ghciで遊んでみる
+ GHCiで遊んでみる
@@ -208,14 +133,6 @@
ghci> True && False
False
-
gchi内で関数を定義する際はletが必要
ghci> let doubleMe x = x + x @@ -232,6 +149,17 @@+ + なぜ Haskell で Web Serivce を書くのか +
++
+
+ +- RubyやPythonなどのインタプリタ言語と比較して高速です +
- 高水準言語で、CやC++、Javaよりも自分の足を撃ち抜きにくいです +
+ + +Warp
@@ -258,32 +186,100 @@ cabal を使えば簡単に入れられます。
+
cabal とは Haskell の Package 管理システムです。+ インストールされているPackage、場所などは以下のコマンドで確かめられます。 +
++$ ghc-pkg list +++ ++ HaskellによるWeb Service +
++ 今日の例題を見ていきます。
+
+ https://gist.github.com/amothic/5938617 ++ この例題は、URLによって出力する結果を変更するWeb Serviceです。 + また、/welcome/worldへアクセスした場合、インクリメントされるcounterが表示されます。 +
++ + HaskellによるWeb Service +
++{-# LANGUAGE OverloadedStrings #-} +import Network.Wai +import Network.HTTP.Types (status200, status404) +import Network.Wai.Handler.Warp (run) +import Control.Monad.Trans (lift) +import Data.IORef (newIORef, atomicModifyIORef) +import Data.ByteString.Lazy.UTF8 (fromString) + +application counter request = function counter + where + function = routes $ pathInfo request + +routes path = findRoute path routeSetting + +findRoute path [] = notFound +findRoute path ((p,f):xs) + | path == p = f + | otherwise = findRoute path xs + +routeSetting = [([], index), + (["hello"], hello), + (["welcome","world"],world)] + +notFound _ = return $ + responseLBS status404 [("Content-type", "text/html")] $ "404 - File Not Found" + +index _ = return $ + responseLBS status200 [("Content-type", "text/html")] $ "index page" + +hello _ = return $ + responseLBS status200 [("Content-type", "text/html")] $ "hello, my name is Tom" + +world counter = do + count <- lift $ incCount counter + return $ responseLBS status200 [("Content-type", "text/html")] $ + fromString $ show count + +incCount counter = atomicModifyIORef counter (\c -> (c+1, c)) + +main = do + counter <- newIORef 0 + run 3000 $ application counter +@@ -292,18 +288,14 @@ 関数の型を確認する - 簡単なプログラム + import
-- hello.hs -
{-# LANGUAGE OverloadedStrings #-} import Network.Wai -import Network.HTTP.Types (status200) +import Network.HTTP.Types (status200, status404) import Network.Wai.Handler.Warp (run) - -application _ = return $ - responseLBS status200 [("Content-Type", "text/plain")] "Hello World" - -main = run 3000 application +import Control.Monad.Trans (lift) +import Data.IORef (newIORef, atomicModifyIORef) +import Data.ByteString.Lazy.UTF8 (fromString) +$ ghci- このソースコードを読み解いていきます。 + 使用するmoduleをimportしています。
- Haskell は型を見れば多くのことが分かる言語です。
+
- ということで、関数の型を見ていきます! + 括弧内に関数名を指定することでその関数のみをimportできます。 ++ プログラムの最初に書かれている OverloadedStrings という言語拡張は、ダブルクオートで囲んだ文字列を、ByteString リテラルとして扱ってくれます。 + ByteStringは、Stringと比較して効率よく文字列を扱います。
- まず、対話モードを開きます。 + Haskell は型を見れば多くのことが分かる言語です。
+
+ ということで、関数の型を見ていきます! ++ 今回作成した例題をロードして、対話モードを開きます。
-$ ghci --- Wai 及び Warp の module を import します。
-
- Wai というのは、Web Application Interface の略で、Web サーバと Web アプリケーション間の共通のプロトコルを取り扱います。 --ghci> :module +Network.Wai -ghci> :module +Network.Wai.Handler.Warp +$ ghci example.hs型を教えて貰うには、:t コマンドに続けて式を入力します。 @@ -313,11 +305,13 @@ run :: Port -> Application -> IO ()
+ まず、main内にあるrunから見ていきます。 +
ghci> :t run run :: Port -> Application -> IO () @@ -326,12 +320,35 @@ run は、Port と Application を受け取って、IO () を返す関数だということが分かります。- IO () は IO モナドを表しています。 - I/O といった副作用を持つ処理を行う時に利用します。 + Haskellのすべての関数は、実は引数を1つだけ取ることになっています。( カリー化関数 ) +
++ 複数取るように見えますが、実際には関数が1つの引数で呼び出されると、その次の引数を受け取る関数を返します。
- IO モナドは中身に直接触ることのできない抽象データ型です。 - 外から触ることを禁止することで、参照透過性を保っています。 + これにより、関数を本来より少ない引数で呼び出したときに部分適用された関数が得られます。 +
+
+ghci> :t run +run :: Port -> Application -> IO () ++
+ IO () は IO モナドを表しています。 +
+ Haskell では、副作用を持つ処理は基本的に許されていません。 +
++ そのため、IO モナドという限られた範囲でのみ行えるようになっています。 +
++ IOモナドは外から触ることのできない抽象データ型です。 + 外から触ることを禁止することで参照透過性を保っています。
Port は、Int の別名です。
@@ -352,31 +369,23 @@
Request を受け取って Response を返す関数を表しています。
+ Portと同じようにtypeで定義されています。 +
++ これはRequest -> ResourceT IO Response に別名 Application をつけているという意味です。 +
+Response は 2つのモナドに包まれています。
ResourceT は、IOのリソースの解放を安全に行うためのものです。
-- Haskell では、関数が副作用を持つことは許されませんが、IO モナドによって IO 操作を処理系に押し付けています。 -
-{-# LANGUAGE OverloadedStrings #-} -import Network.Wai -import Network.HTTP.Types (status200) -import Network.Wai.Handler.Warp (run) - -application _ = return $ - responseLBS status200 [("Content-Type", "text/plain")] "Hello World" - -main = run 3000 application -
型情報から以下のことが分かります。
@@ -388,25 +397,31 @@ 実際の動作としては、この関数 run は受け取った Port 3000番で、Application を実行します。- 次に、Application の実装を見ていきます。 + 次に、Request を受け取って Response を返す関数である application の実装を見ていきます。
+ applicationでは、requestによって呼び出す関数を振り分けます。 + where は関数内で使う変数を定義するもので、今回 function には呼び出す関数が入ります。 +
-application _ = return $ - responseLBS status200 [("Content-Type", "text/plain")] "Hello World" +application counter request = function counter + where + function = routes $ pathInfo request
- まず引数で渡される Request は _ (Underscore) となっているので、使用していません。 -
-- return の後ろに付いている $ は、関数適用演算子といって括弧の数を減らすのに役たちます。 + routes の後ろに付いている $ は、関数適用演算子といって括弧の数を減らすのに役たちます。 普通の関数適用は非常に優先順位が高いのですが、$ は最も低い優先順位を持ちます。
++($) :: (a -> b) -> a -> b +f $ x = f x +
下記の2つのコードは同じ結果になります。
@@ -417,110 +432,16 @@-ghci> :t responseLBS -responseLBS - :: Status - -> ResponseHeaders - -> Data.ByteString.Lazy.Internal.ByteString - -> Response -+
- Statusと、ResponseHeaders、ByteStringを受け取り、Responseを返します。 -
-
- 簡単に説明すると、文字列からResponseを構築するためのコンストラクターです。
+ request には、clientが送る様々な情報が含まれています。
+ その中には pathInfo という、どこの path へアクセスしてきたかの情報があります。
+ この情報をroutes関数に渡すことで呼び出す関数を振り分けています。
- ByteStringは、Stringと比較して効率よく文字列を扱います。 - プログラムの最初に書かれている OverloadedStrings という言語拡張は、ダブルクオートで囲んだ文字列を、ByteString リテラルとして扱ってくれます。 -
--{-# LANGUAGE OverloadedStrings #-} --
- hello.hs -
--{-# LANGUAGE OverloadedStrings #-} -import Network.Wai -import Network.HTTP.Types (status200) -import Network.Wai.Handler.Warp (run) - -application _ = return $ - responseLBS status200 [("Content-Type", "text/plain")] "Hello World" - -main = run 3000 application --
- 実行方法は2つあります。 -
--
-$ runghc Hello.hs -- -
-
-$ ghc --make Hello.hs -$ ./Hello -- -
- Hello Worldと表示されれば成功です。 -
-- 次に単純な Routing を行うサイトを実装してみたいと思います。 -
-- http://localhost:3000/ の後の URL の Path によって 出力する結果を変更してみます。 -
-
- Application が受け取る Request には、clientが送る様々な情報が含まれています。
- その中には pathInfo という、どこの path へアクセスしてきたかの情報があります。
-
+ 以下はRequestに含まれる情報です。
- routes.hs -
-application request = return $ - routes $ pathInfo request - -routes path = findRoute path routeSetting - -findRoute path [] = notFound -findRoute path ((p,f):xs) - | path == p = f - | otherwise = findRoute path xs -routeSetting = [([], index), - (["hello"], hello), - (["welcome","world"],world)] -notFound = - responseLBS status404 [("Content-type", "text/html")] $ "404 - File Not Found" -index = - responseLBS status200 [("Content-type", "text/html")] $ "index page" -hello = - responseLBS status200 [("Content-type", "text/html")] $ "hello, my name is Tom" -world = - responseLBS status200 [("Content-type", "text/html")] $ "Welcome to Underground" - -main = run 3000 application -- -
- 実は超簡単です。 -
--pathInfo request --
- pathInfo という関数に Request を渡すだけ! + requestは、data キーワードを使って定義されています。 + Haskell では、data キーワードを使って自作のデータ型を作ることができます。
- Haskell では、レコード構文というデータ型のフィールドにアクセスするためのコードを自動生成する仕組みがあります。 - レコード構文で、データ型を定義するとデータ型が作られるのと同時に、フィールド名でフィールドを取得する関数たちが自動で作られます。 + pathInfoという関数は、requestからpathに関する情報を抜き出しますが、これはレコード構文というものを利用しています。
-
-ghci> :t pathInfo -pathInfo :: Request -> [Data.Text.Internal.Text] -+ 以下にレコード構文の例を表示します。 + レコード構文を使うと簡単にアクセサ関数を定義できます。 -
+-- レコード構文を使わない場合 data Person = Person String String Int Float String String deriving (Show) @@ -611,12 +484,8 @@ lastName (Person _ lastname _ _ _ _) = lastname age :: Person -> Int age (Person _ _ age _ _ _) = age -height :: Person -> Float -height (Person _ _ _ height _ _) = height -phoneNumber :: Person -> String -phoneNumber (Person _ _ _ _ number _) = number -flavor :: Person -> String -flavor (Person _ _ _ _ _ flavor) = flavor + +( 省略 ) -- レコード構文 data Person = Person { firstName :: String @@ -626,140 +495,94 @@ , phoneNumber :: String , flavor :: String } deriving (Show)-
- まず、application で request を取り routes という関数に path を渡すように変更します。 -
--application request = return $ - routes $ pathInfo request --
- routes は、path を受け取って response を返す関数です。 + routes 関数では、routeSettingを付け足して、findRoute関数を呼び出しています。
routes path = findRoute path routeSetting
- routes では、routeSetting という List から path が一致するもの探します。 + routeSetting は、path と関数を記載した List になっています。 +
++routeSetting = [([], index), + (["hello"], hello), + (["welcome","world"],world)] ++
+ この情報を使って、返す関数を決めます。
- findRoute では、再帰的に List を探索しています。
+ findRoute は再帰的にListを探索しています。
+
+ 一致するものがなければ、notFound という関数を返します。
+ findRoute が二行にわたって書かれているのはパターンマッチを利用しているためです。
+
+ findRouteを呼ぶと、パターンが上から下の順で試されます。
+ 渡された値が指定されたパターンと一致すると、対応する関数の本体が使われ、残りのパターンは無視されます。
+
- いくつか定義してみます。
+ notFoundは、404 - File Not Found と表示する関数になります。
+ indexは、index page と表示する関数になります。
- routes.hs
+ responseLBS とは?
+
+ Statusと、ResponseHeaders、ByteStringを受け取り、Responseを返します。
+
+ 簡単に説明すると、文字列からResponseを構築するためのコンストラクターです。
- シンプルなシステムですが、実際に動くのか確かめてみます。
-
- "index page" と表示されるはず
- "hello, my name is Tom" と表示されるはず
- "welcome to Underground" と表示されるはず
- 一致するpathがないので、"404 - File Not Found" と表示されるはず
- 最後に、Counter の実装を行なってみたいと思います。
-
- アクセスするたびに、Count がインクリメントされていくようなサイトを作成します。
+ アクセスするたびに、Count がインクリメントされていくようなページを作ります。
ここでは、Thread-safe な State である Data.IORef を用います。
@@ -768,26 +591,6 @@
- Haskell では、変数の更新のような副作用を持つ処理は基本的に許されていません。
-
- そのため、IO モナドという限られた範囲でのみ行えるようになっています。
-
- IOモナドは中身に直接触ることのできない抽象データ型です。
- 状態を外から触ることを禁止することで参照透過性を保っています。
-
- また、Haskell は遅延評価ですが、初期化などIOでは実行順序が重要になってきます。
- モナドのbindを利用して、計算の実行順序を保証します。
-
@@ -806,27 +609,6 @@
- 一致するものがなければ、notFound という関数を返します。
- 一致するものがあれば、routeSetting に記載された関数を返します。
+
+ findRoute
+
findRoute path [] = notFound
findRoute path ((p,f):xs)
| path == p = f
| otherwise = findRoute path xs
-
-routeSetting = [([], index),
- (["hello"], hello),
- (["welcome","world"],world)]
+
+ 一致するものがあれば、routeSetting に記載された関数を返します。
+
- response関数を定義しよう
-
+
+ Response を返す関数の実装
+
-notFound =
+notFound _ = return $
responseLBS status404 [("Content-type", "text/html")] $ "404 - File Not Found"
-index =
+index _ = return $
responseLBS status200 [("Content-type", "text/html")] $ "index page"
-
-hello =
- responseLBS status200 [("Content-type", "text/html")] $ "hello, my name is Tom"
-
-world =
- responseLBS status200 [("Content-type", "text/html")] $ "Welcome to Underground"
-
- 完成!
-
-application request = return $
- routes $ pathInfo request
-
-routes path = findRoute path routeSetting
-
-findRoute path [] = notFound
-findRoute path ((p,f):xs)
- | path == p = f
- | otherwise = findRoute path xs
-routeSetting = [([], index),
- (["hello"], hello),
- (["welcome","world"],world)]
-notFound =
- responseLBS status404 [("Content-type", "text/html")] $ "404 - File Not Found"
-index =
- responseLBS status200 [("Content-type", "text/html")] $ "index page"
-hello =
- responseLBS status200 [("Content-type", "text/html")] $ "hello, my name is Tom"
-world =
- responseLBS status200 [("Content-type", "text/html")] $ "Welcome to Underground"
-
-main = run 3000 application
+ghci> :t responseLBS
+responseLBS
+ :: Status
+ -> ResponseHeaders
+ -> Data.ByteString.Lazy.Internal.ByteString
+ -> Response
+
- 実際に動かしてみる!
-
-
- http://localhost:3000/
-
- http://localhost:3000/hello
-
+ Counter の実装
+
- http://localhost:3000/welcome/world
-
- http://localhost:3000/hogehoge
-
- Counter
-
-
- 変数の更新はできなかったんじゃ?
-
-
Data.IORefの使い方
Data.IORefの使い方
-
--- IORef Int という型のデータを作製する
-counter <- newIORef 0
-
--- データの更新を atomic に行う
--- atomicModifyIORef には、更新したい IORef a 型の変数と、
--- IORef が持つ値を受け取って
--- ( 更新後の値, 戻り値にしたい値 ) というタプルを返す関数を渡す
-incCount:: IORef a -> IO a
-incCount counter = atomicModifyIORef counter (\c -> (c+1, c))
-
--- 現在のデータの値を受け取る
-currentNum <- readIORef counter
-
-
- では、このIORefを使ってCounterを作りましょう!
-
-- IORef Int という型のデータを作製する
@@ -844,129 +626,146 @@
- counter.hs + まず、main内でcouterを初期化します。 +
++counter <- newIORef 0 ++
+ world 関数で、counterを利用します。 +
+world counter = do + count <- lift $ incCount counter + return $ responseLBS status200 [("Content-type", "text/html")] $ + fromString $ show count + +incCount counter = atomicModifyIORef counter (\c -> (c+1, c)) ++
+ incCountでは、atomicModifyIORefを使って、counterをインクリメントしています。 +
++ また、incCountを、ResourceTのモナド内に持ち込むためliftを行なっています。 +
+{-# LANGUAGE OverloadedStrings #-} import Network.Wai -import Network.HTTP.Types (status200) +import Network.HTTP.Types (status200, status404) import Network.Wai.Handler.Warp (run) -import Control.Monad.Trans (liftIO, lift) +import Control.Monad.Trans (lift) import Data.IORef (newIORef, atomicModifyIORef) import Data.ByteString.Lazy.UTF8 (fromString) -application counter request = do - count <- lift $ incCount counter - return $ responseLBS status200 [("Content-type", "text/html")] $ +application counter request = function counter + where + function = routes $ pathInfo request + +routes path = findRoute path routeSetting + +findRoute path [] = notFound +findRoute path ((p,f):xs) + | path == p = f + | otherwise = findRoute path xs + +routeSetting = [([], index), + (["hello"], hello), + (["welcome","world"],world)] + +notFound _ = return $ + responseLBS status404 [("Content-type", "text/html")] $ "404 - File Not Found" + +index _ = return $ + responseLBS status200 [("Content-type", "text/html")] $ "index page" + +hello _ = return $ + responseLBS status200 [("Content-type", "text/html")] $ "hello, my name is Tom" + +world counter = do + count <- lift $ incCount counter + return $ responseLBS status200 [("Content-type", "text/html")] $ fromString $ show count incCount counter = atomicModifyIORef counter (\c -> (c+1, c)) -main = do +main = do counter <- newIORef 0 run 3000 $ application counter+
+ 実行方法は2つあります。 +
++
+$ runghc example.hs ++ +
+
+$ ghc --make example.hs +$ ./example +
- mainでは、counterの初期化を行なっています。 + シンプルなシステムですが、実際に動くのか確かめてみます。
- それをrunとdo構文で糊付けしています。 -
--main = do - counter <- newIORef 0 - run 3000 $ application counter --
- applicationでは、引数をひとつ増やして、初期化されたIORefを受け取れるようにしています。 -
-
- また、incCountを、ResourceTのモナド内に持ち込むためliftを行なっています。
+ "index page" と表示されるはず
+ http://localhost:3000/
- incCountでは、atomicModifyIORefを使って、counterをインクリメントしています。
+ "hello, my name is Tom" と表示されるはず
+ http://localhost:3000/hello
-application counter request = do - count <- lift $ incCount counter - return $ responseLBS status200 [("Content-type", "text/html")] $ - fromString $ show count - -incCount counter = atomicModifyIORef counter (\c -> (c+1, c)) --
- counter.hs -
-{-# LANGUAGE OverloadedStrings #-} -import Network.Wai -import Network.HTTP.Types (status200) -import Network.Wai.Handler.Warp (run) -import Control.Monad.Trans (liftIO, lift) -import Data.IORef (newIORef, atomicModifyIORef) -import Data.ByteString.Lazy.UTF8 (fromString) - -application counter request = do - count <- lift $ incCount counter - return $ responseLBS status200 [("Content-type", "text/html")] $ - fromString $ show count - -incCount counter = atomicModifyIORef counter (\c -> (c+1, c)) - -main = do - counter <- newIORef 0 - run 3000 $ application counter -+ インクリメントされる counter が表示されるはず
+ 一致するpathがないので、"404 - File Not Found" と表示されるはず
+ http://localhost:3000/hogehoge
- アクセスするたびに表示される数字がインクリメントされていくはずです。 + ここまで書いてきて、Haskellのプログラムはだいぶ短く書けることに気がつくと思います。
- Chromeだと、毎回faviconにアクセスされるせいで、countが+2されていきます… -
-- あれ、Haskellのプログラムって短くね? -
-- そうです、圧倒的な記述力も特徴なんです。 + 圧倒的な記述力も特徴のひとつです。
速くて安全なHaskellで、あなたもWeb Serviceを作って見ませんか?