シンプルなHaskellブログ甚のRESTサヌバヌ

しばらく前、私は動的型付けの蚀語に完党にうんざりしおいたした。 Haskellは、コヌドの矎しさず、玔粋な機胜ず副䜜甚を明確に分離するずいう劥協のない欲求が奜きでした。 私はHaskellの本をいく぀か飲み干し、すでに䜕かを曞く時だず刀断したした。



そしお、倱望が私を埅っおいたした。こんにちは、world-a以倖は䜕も曞くこずができたせんでした。 ぀たり findなどのコン゜ヌルナヌティリティの䜜成方法を倧たかに想像しおいたしたが、IOずの最初の出䌚いはすべおのアむデアを砎壊したした。 Haskellには倚くのラむブラリがあるようですが、それらのドキュメントはほずんどありたせん。 兞型的な問題を解決する䟋も非垞に少ない。



症状は明確であり、蚺断は簡単です。緎習䞍足です。 そしお、Haskellにずっお、これは非垞に苊痛です。 蚀語は非垞に珍しいです。 私がClojureをよく知っおいるずいう事実でさえ、私をあたり助けたせんでした、なぜなら Clojureは関数に重点を眮いおおり、Haskellはその型に焊点を圓おおいたす。



倚くの新参者がHaskellでの緎習䞍足の問題に盎面しおいるず思いたす。 むンタヌフェむスなしで完党に䜕かを曞くこずは、どういうわけか面癜くなく、初心者のHaskelist甚のデスクトップたたはWebアプリケヌションを䜜成するこずは非垞に困難です。 この蚘事では、特にHaskellを緎習したいが、どの方法でアプロヌチするのかわからない人のために、HaskellでWebアプリケヌションサヌバヌを䜜成する方法の簡単な䟋を提䟛したす。



最もせっかちな人のために゜ヌスはこちらです。



すぐに蚀いたすこれは別のYesodチュヌトリアルではありたせん。 このフレヌムワヌクは、Webアプリケヌションを正しく䜜成する方法に関するアむデアを決定するものであり、私はすべおに同意するわけではありたせん。 したがっお、ベヌスは小さなScottyラむブラリヌになり、 Warp Webサヌバヌ甚の矎しいルヌト蚘述構文を提䟛したす。



挑戊する



シンプルなブログ甚のWebアプリケヌションサヌバヌを蚭蚈したす。 次のルヌトが利甚可胜になりたす。



「/ admin」で始たるすべおのルヌトには、ナヌザヌ認蚌が必芁です。 ステヌトレスサヌビスの堎合、 基本認蚌を䜿甚するず非垞に䟿利です。 各リク゚ストには、ナヌザヌのナヌザヌ名ずパスワヌドが含たれおいたす。



䜕が必芁ですか





建築



アヌキテクチャを実装するには、次のラむブラリの䜿甚を提案したす。



アプリケヌションをモゞュヌルに分割したす。



降りる



アプリケヌション甚の単玔なcabalプロゞェクトを䜜成したしょう。



mkdir hblog cd hblog cabal init
      
      





ここでいく぀かの質問に答える必芁がありたす。プロゞェクトの皮類は実行可胜ファむルを遞択し、メむンファむル-Main.hs、゜ヌスディレクトリ-srcを遞択したす。 hblog.cabalファむルのbuild-dependsに远加する必芁があるラむブラリを以䞋に瀺したす。



  base >= 4.6 && < 4.7 , scotty >= 0.9.1 , bytestring >= 0.9 && < 0.11 , text >= 0.11 && < 2.0 , mysql >= 0.1.1.8 , mysql-simple >= 0.2.2.5 , aeson >= 0.6 && < 0.9 , HTTP >= 4000.2.19 , transformers >= 0.4.3.0 , wai >= 3.0.2.3 , wai-middleware-static >= 0.7.0.1 , wai-extra >= 3.0.7 , resource-pool >= 0.2.3.2 , configurator >= 0.3.0.0 , MissingH >= 1.3.0.1
      
      





ここで、ラむブラリのバヌゞョンずその䟝存関係の混乱を回避するために、サンドボックスを䜜成したす。



  cabal sandbox init cabal install —dependencies-only
      
      





src / Main.hsファむルを忘れずに䜜成しおください。



最小限のScotty Webアプリケヌションがどのように機胜するかを芋おみたしょう。 このマむクロフレヌムワヌクの䜿甚に関するドキュメントず䟋は非垞に優れおいるため、䞀目ですべおが明らかになりたす。 たた、シナトラ、コンポゞュレ、たたはスカラトラの経隓がある堎合は、幞運だず考えおください。 この経隓はここで完党に圹立ちたす。



これは、最小のsrc / Main.hsの倖芳です。



 {-# LANGUAGE OverloadedStrings #-} import Web.Scotty import Data.Monoid (mconcat) main = scotty 3000 $ do get "/:word" $ do beam <- param "word" html $ mconcat ["<h1>Scotty, ", beam, " me up!</h1>"]
      
      





コヌドの最初の行は、初心者を驚かせる可胜性がありたすオヌバヌロヌドされた行は他​​に䜕ですか これから説明したす。



私は、他の倚くの人ず同じように、「 より良い利益のためにHaskellを孊がう 」ず「 Real World Haskell 」ずいう本からHaskellを孊び始めたので、テキスト凊理はすぐに倧きな問題になりたした。 Haskellのテキストの操䜜に関する最良の説明は、第10章の最初のHaskellで芋぀けたした。



非垞に短い堎合、実際には3぀の基本的なタむプの文字列デヌタが䜿甚されたす。



OverloadedStringsの芋出しに戻りたす。 問題は、いく぀かのタむプの文字列デヌタが存圚する堎合、゜ヌスはT.pack "Hello"などの呌び出しでいっぱいになり、トヌクン "Hello"をテキストに倉換する必芁があるこずです。 たたは、トヌクンをByteStringに倉換する必芁があるB.pack“ Hello”。 この構文ガベヌゞを䜿甚するために、文字列トヌクンを目的の文字列型に独立しお倉換するOverloadedStringsディレクティブを次に瀺したす。



Main.hsファむル



䞻な機胜



 main :: IO () main = do --      application.conf,         loadedConf <- C.load [C.Required "application.conf"] dbConf <- makeDbConfig loadedConf case dbConf of Nothing -> putStrLn "No database configuration found, terminating..." Just conf -> do --    (    — 5 ,      -- 10) pool <- createPool (newConn conf) close 1 5 10 --   Scotty scotty 3000 $ do --       «static» middleware $ staticPolicy (noDots >-> addBase "static") --   .    logStdout  logStdoutDev middleware $ logStdoutDev --       middleware $ basicAuth (verifyCredentials pool) "Haskell Blog Realm" { authIsProtected = protectedResources } get "/articles" $ do articles <- liftIO $ listArticles pool articlesList articles --     :id       get "/articles/:id" $ do id <- param "id" :: ActionM TL.Text maybeArticle <- liftIO $ findArticle pool id viewArticle maybeArticle --      Article     Article   post "/admin/articles" $ do article <- getArticleParam insertArticle pool article createdArticle article put "/admin/articles" $ do article <- getArticleParam updateArticle pool article updatedArticle article delete "/admin/articles/:id" $ do id <- param "id" :: ActionM TL.Text deleteArticle pool id deletedArticle id
      
      





configuratorパッケヌゞを䜿甚しお、アプリケヌションを構成したす。 蚭定をapplication.confファむルに保存し、その内容を以䞋に瀺したす。



 database { name = "hblog" user = "hblog" password = "hblog" }
      
      





接続プヌルには、リ゜ヌスプヌルラむブラリを䜿甚したす。 デヌタベヌスぞの接続は高䟡なので、リク゚ストごずに䜜成するのではなく、叀いものを再利甚する機䌚を䞎えおください。 createPool関数のタむプは次のずおりです。



 createPool :: IO a -> (a -> IO ()) -> Int -> NominalDiffTime -> Int -> IO (Pool a) createPool create destroy numStripes idleTime maxResources
      
      





ここで、createおよびdestroyはデヌタベヌス接続を䜜成および終了するための関数、numStripesは個別の接続サブプヌルの数、idleTimeは未䜿甚の接続の存続時間秒、maxResourcesはサブプヌル内の最倧接続数です。



接続を開くには、newConn関数Db.hsからを䜿甚したす。



 data DbConfig = DbConfig { dbName :: String, dbUser :: String, dbPassword :: String } deriving (Show, Generic) newConn :: DbConfig -> IO Connection newConn conf = connect defaultConnectInfo { connectUser = dbUser conf , connectPassword = dbPassword conf , connectDatabase = dbName conf }
      
      





DbConfig自䜓は次のように䜜成されたす。



 makeDbConfig :: C.Config -> IO (Maybe Db.DbConfig) makeDbConfig conf = do name <- C.lookup conf "database.name" :: IO (Maybe String) user <- C.lookup conf "database.user" :: IO (Maybe String) password <- C.lookup conf "database.password" :: IO (Maybe String) return $ DbConfig <$> name <*> user <*> password
      
      





入力には、application.confから読み取っお解析するData.Configurator.Configが枡され、出力はIOシェルに囲たれたDbConfigである可胜性がありたす。



初心者向けのこのような゚ントリは少しわかりにくいかもしれたせんが、ここで䜕が起こっおいるのか説明しようず思いたす。

匏タむプC.lookup conf "database.name"は、おそらくIOで囲たれた文字列です。 次のようにIOから抜出できたす。



 name <- C.lookup conf "database.name" :: IO (Maybe String)
      
      





したがっお、定数名、ナヌザヌ、パスワヌドの皮類は倚分文字列です。



DbConfigデヌタコンストラクタヌのタむプは次のずおりです。



 DbConfig :: String -> String -> String -> DbConfig
      
      





この関数は3行の入力を受け取り、DbConfigを返したす。



関数のタむプ<$>は次のずおりです。



 (<$>) :: Functor f => (a -> b) -> fa -> fb
      
      





぀たり 通垞の関数、ファンクタヌを取り、その倀に適甚された関数を持぀ファンクタヌを返したす。 芁するに、これは通垞のマップです。



DbConfig <$> name゚ントリは、名前から文字列を取埗し型のタむプはMaybe String、DbConfigコンストラクタヌの最初のパラメヌタヌに倀を割り圓お、Maybeシェルでカリヌ化されたDbConfigを返したす。



 DbConfig <$> name :: Maybe (String -> String -> DbConfig)
      
      





ここでは、すでに1぀の文字列の転送が少なくなっおいるこずに泚意しおください。



タむプ<*>は<$>に䌌おいたす



 (<*>) :: Applicative f => f (a -> b) -> fa -> fb
      
      





圌は倀が関数であるファンクタヌを取り、別のファンクタヌを取り、最初のファンクタヌからの関数を2番目のファンクタヌから倀に適甚し、新しいファンクタヌを返したす。



したがっお、゚ントリDbConfig <$> name <*> userのタむプは次のずおりです。



 DbConfig <$> name <*> user :: Maybe (String -> DbConfig)
      
      





パスワヌドを入力する最埌の文字列パラメヌタヌが残っおいたす。



 DbConfig <$> name <*> user <*> password :: Maybe DbConfig
      
      





認蚌



メむン関数では、最埌の耇雑な構造が残りたした-これはミドルりェアbasicAuthです。 basicAuth関数のタむプは次のずおりです。



 basicAuth :: CheckCreds -> AuthSettings -> Middleware
      
      





最初のパラメヌタヌはデヌタベヌス内のナヌザヌの存圚をチェックする関数で、2番目は認蚌保護が必芁なルヌトを決定したす。 それらのタむプ



 type CheckCreds = ByteString -> ByteString -> ResourceT IO Bool data AuthSettings = AuthSettings { authRealm :: !ByteString , authOnNoAuth :: !(ByteString -> Application) , authIsProtected :: !(Request -> ResourceT IO Bool) }
      
      





AuthSettingsデヌタ型は非垞に耇雑であり、もう少し詳しく知りたい堎合は、こちらの゜ヌスを参照しおください 。 ここでは、authIsProtectedずいう1぀のパラメヌタヌのみに関心がありたす。 これは、リク゚ストにより、認蚌を芁求するかどうかを決定できる関数です。 ブログの実装は次のずおりです。



 protectedResources :: Request -> IO Bool protectedResources request = do let path = pathInfo request return $ protect path where protect (p : _) = p == "admin" protect _ = False
      
      





pathInfo関数には次のタむプがありたす。



 pathInfo :: Request -> [Text]
      
      





リク゚ストを受け取り、リク゚ストルヌトを区切り文字「/」で郚分文字列に分割した埌に取埗した文字列のリストを返したす。

したがっお、リク゚ストが「/ admin」で始たる堎合、protectedResources関数はIO Trueを返し、認蚌を芁求したす。



ただし、ナヌザヌずパスワヌドを確認するverifyCredentials関数は、デヌタベヌスずの盞互䜜甚を参照するため、以䞋のずおりです。



デヌタベヌスの盞互䜜甚



接続プヌルを䜿甚しおデヌタベヌスからデヌタを抜出するナヌティリティ関数



 fetchSimple :: QueryResults r => Pool M.Connection -> Query -> IO [r] fetchSimple pool sql = withResource pool retrieve where retrieve conn = query_ conn sql fetch :: (QueryResults r, QueryParams q) => Pool M.Connection -> q -> Query -> IO [r] fetch pool args sql = withResource pool retrieve where retrieve conn = query conn sql args
      
      





パラメヌタなしのク゚リにはfetchSimple関数を䜿甚し、パラメヌタ付きのク゚リにはfetchSimple関数を䜿甚する必芁がありたす。 デヌタの倉曎は、execSql関数を䜿甚しお実行できたす。



 execSql :: QueryParams q => Pool M.Connection -> q -> Query -> IO Int64 execSql pool args sql = withResource pool ins where ins conn = execute conn sql args
      
      





トランザクションを䜿甚する必芁がある堎合、execSqlT関数を次に瀺したす。



 execSqlT :: QueryParams q => Pool M.Connection -> q -> Query -> IO Int64 execSqlT pool args sql = withResource pool ins where ins conn = withTransaction conn $ execute conn sql args
      
      





たずえば、フェッチ関数を䜿甚するず、ログむンによっおデヌタベヌス内のナヌザヌのパスワヌドのハッシュを怜玢できたす。



 findUserByLogin :: Pool Connection -> String -> IO (Maybe String) findUserByLogin pool login = do res <- liftIO $ fetch pool (Only login) "SELECT * FROM user WHERE login=?" :: IO [(Integer, String, String)] return $ password res where password [(_, _, pwd)] = Just pwd password _ = Nothing
      
      





Auth.hsモゞュヌルで必芁です



 verifyCredentials :: Pool Connection -> B.ByteString -> B.ByteString -> IO Bool verifyCredentials pool user password = do pwd <- findUserByLogin pool (BC.unpack user) return $ comparePasswords pwd (BC.unpack password) where comparePasswords Nothing _ = False comparePasswords (Just p) password = p == (md5s $ Str password)
      
      





ご芧のずおり、パスワヌドハッシュがデヌタベヌスで芋぀かった堎合、md5アルゎリズムを䜿甚しお゚ンコヌドされたリク゚ストからパスワヌドにマッピングできたす。



ただし、デヌタベヌスにはナヌザヌだけでなく、ブログが䜜成、線集、衚瀺できる蚘事も保存されたす。 Domain.hsファむルで、id title bodyTextフィヌルドを䜿甚しおArticleデヌタ型を定矩したす。



 data Article = Article Integer Text Text deriving (Show)
      
      





これで、このタむプのデヌタベヌスにCRUD関数を定矩できたす。



 listArticles :: Pool Connection -> IO [Article] listArticles pool = do res <- fetchSimple pool "SELECT * FROM article ORDER BY id DESC" :: IO [(Integer, TL.Text, TL.Text)] return $ map (\(id, title, bodyText) -> Article id title bodyText) res findArticle :: Pool Connection -> TL.Text -> IO (Maybe Article) findArticle pool id = do res <- fetch pool (Only id) "SELECT * FROM article WHERE id=?" :: IO [(Integer, TL.Text, TL.Text)] return $ oneArticle res where oneArticle ((id, title, bodyText) : _) = Just $ Article id title bodyText oneArticle _ = Nothing insertArticle :: Pool Connection -> Maybe Article -> ActionT TL.Text IO () insertArticle pool Nothing = return () insertArticle pool (Just (Article id title bodyText)) = do liftIO $ execSqlT pool [title, bodyText] "INSERT INTO article(title, bodyText) VALUES(?,?)" return () updateArticle :: Pool Connection -> Maybe Article -> ActionT TL.Text IO () updateArticle pool Nothing = return () updateArticle pool (Just (Article id title bodyText)) = do liftIO $ execSqlT pool [title, bodyText, (TL.decodeUtf8 $ BL.pack $ show id)] "UPDATE article SET title=?, bodyText=? WHERE id=?" return () deleteArticle :: Pool Connection -> TL.Text -> ActionT TL.Text IO () deleteArticle pool id = do liftIO $ execSqlT pool [id] "DELETE FROM article WHERE id=?" return ()
      
      





ここで最も重芁なのは、insertArticleおよびupdateArticle関数です。 入力ずしおMaybe Articleを受け入れ、デヌタベヌス内の察応する゚ントリを挿入/曎新したす。 しかし、この倚分蚘事はどこで入手できたすか



すべおが単玔で、ナヌザヌはJSONで゚ンコヌドされたArticleをPUTたたはPOSTリク゚ストの本文で枡す必芁がありたす。 JSON内倖の蚘事を゚ンコヌドおよびデコヌドするための関数は次のずおりです。



 instance FromJSON Article where parseJSON (Object v) = Article <$> v .:? "id" .!= 0 <*> v .: "title" <*> v .: "bodyText" instance ToJSON Article where toJSON (Article id title bodyText) = object ["id" .= id, "title" .= title, "bodyText" .= bodyText]
      
      





JSONを凊理するには、aesonラむブラリを䜿甚したす 。詳现に぀いおは、 こちらをご芧ください 。



ご芧のずおり、デコヌド時にはidフィヌルドはオプションであり、JSONの行にない堎合、デフォルト倀は0に蚭定されたす。Article゚ントリの䜜成時にはidフィヌルドは存圚したせん。 idはデヌタベヌス自䜓を䜜成する必芁がありたす。 ただし、idは曎新リク゚ストに含たれたす。



デヌタ提瀺



Main.hsファむルに戻り、リク゚ストパラメヌタを取埗する方法を芋おみたしょう。 param関数を䜿甚しお、ルヌトからパラメヌタヌを取埗できたす。



 param :: Parsable a => TL.Text -> ActionM a
      
      





そしお、リク゚スト関数はbody関数で取埗できたす



 body :: ActionM Data.ByteString.Lazy.Internal.ByteString
      
      





リク゚ストの本文を取埗しお解析し、Maybeを返すこずができる関数を以䞋に瀺したす



 getArticleParam :: ActionT TL.Text IO (Maybe Article) getArticleParam = do b <- body return $ (decode b :: Maybe Article) where makeArticle s = ""
      
      





最埌に残ったのは、クラむアントにデヌタを返すこずです。 これを行うには、Views.hsファむルで次の関数を定矩したす。



 articlesList :: [Article] -> ActionM () articlesList articles = json articles viewArticle :: Maybe Article -> ActionM () viewArticle Nothing = json () viewArticle (Just article) = json article createdArticle :: Maybe Article -> ActionM () createdArticle article = json () updatedArticle :: Maybe Article -> ActionM () updatedArticle article = json () deletedArticle :: TL.Text -> ActionM () deletedArticle id = json ()
      
      







サヌバヌのパフォヌマンス



テストには、8GBのメモリずクアッドコアIntel Core i7を搭茉したSamsung 700Zラップトップを䜿甚したした。



少なくずも䜕かず比范できるようにするために、Java 7ずSpring 4でTomcat 7 Webサヌバヌを䜿甚しおたったく同じサヌバヌを実装し、次の数字を受け取りたした。



結論



Haskellの緎習が䞍足しおいお、その䞊でWebアプリケヌションを䜜成しようずする堎合、蚘事-蚘事で説明されおいる1぀の゚ンティティに察するCRUD操䜜を䜿甚した単玔なサヌバヌの䟋を芋぀けるこずができたす。 アプリケヌションはJSON RESTサヌビスずしお実装され、安党なルヌトでの基本認蚌が必芁です。 MySQL DBMSはデヌタストレヌゞに䜿甚され、接続プヌルはパフォヌマンスを改善するために䜿甚されたす。 アプリケヌションはセッションに状態を保存しないため、氎平方向ぞのスケヌリングは非垞に簡単です。さらに、ステヌトレスサヌバヌはマむクロサヌビス アヌキテクチャの開発に最適です。



Haskellを䜿甚しおJSON RESTサヌバヌを開発するこずで、短くお矎しい゜ヌスを取埗できたした。これは、ずりわけ、リファクタリング、倉曎、远加に倚くの䜜業を必芁ずしないため、保守が容易です。 コンパむラは、すべおの倉曎の正確性をチェックしたす。 Haskellを䜿甚するこずの欠点は、Javaで䜜成された同様のサヌビスず比范しお、結果のWebサヌビスのパフォヌマンスがそれほど高くないこずです。



PS



コメントのアドバむスに基づいお、远加のテストを実斜したした。 スレッド数をN = 8に倉曎しおも、パフォヌマンスには圱響したせん。 Nがさらに枛少するず、パフォヌマンスが䜎䞋したす。 私のラップトップには8぀の論理コアがありたす。



別の興味深いこず。 デヌタベヌスぞのレコヌドの保存を無効にするず、Haskellに察するサヌビスの応答の平均遅延は6ミリ秒に䜎䞋したす。Javaの同様のサヌビスでは、今回は80ミリ秒です。 ぀たり 瀺されおいるプロゞェクトのボトルネックはデヌタベヌスずの盞互䜜甚です。これをオフにするず、HaskellはJavaの同様の機胜よりも13倍高速になりたす。 メモリ消費量も数倍䜎くなりたす。400MBに察しお玄80MBです。



All Articles