如何用标准化的,通用的纯函数编程语言Haskell来构建Web?Haskell构建的Web拥有着什么样的优点呢?让我们来看看构建的过程吧。
作者%20|%20William%20Yao
译者%20|弯月,责编%20|%20maozz
出品%20|%20CSDN(ID:CSDNnews)
以下为译文:
Haskell有大量的库可用于满足基本的后台需求,从日志输出到数据库访问,再到Web服务器的定义和路由,应有尽有。
拥有选择的自由固然很好,但如果你刚刚接触这个领域,那么大量的选择可能会让你目不暇接。也许你没法自信地判断出这些选择之间的区别。例如,你需要查询数据库。
那么,你需要Squeal提供的列名强保证,以及深度SQL嵌入功能吗?还是青睐Opaleye在保证类型安全的前提下的简单性?或者只是使用简单的postgresql-simple就好?还是使用Selda?或者……
我用最简单的库写了一个Web应用程序,一方面是想告诉你,你不需要担心技术栈是否足够先进,另一方面也是我自己的学习过程。
如果你不知道怎样用Haskell构建真正的应用程序,为什么不去学习呢?我会尽可能保持代码的简单性。
点击此处可查看源代码(https://gitlab.com/williamyaoh/haskell-web-stack)。
下面我们来看一看我选择的这些库,以及它们的功能,还有这个应用程序的功能。
1.这个Web应用程序究竟是什么呢?
它是一个网站,用户可以在这个网站上创建定时器和做笔记。
我举个例子说明该网站的用途:某人想设置多个定时器来跟踪多个反应物的过程,同时用笔记来记录他们需要关心的事情、下次制作配方时需要改进的事情,等等。
再举一个例子,某个人在玩MOBA游戏,比如英雄联盟或者刀塔2,他们可以在另一台显示器上打开一个页面来跟踪关键技能的冷却时间,同时用笔记记录宏操作、对手的组合,以及团战时需要关注技的冷却等。
在这个应用程序中我将演示:
会话,用户可以刷新页面,或者关闭页面后再打开,但在打开之后应该看到相同的内容。
持久化和数据库访问,我们需要将每个用户的定时器和笔记保存下来。另一个需求是定时器需要维持剩余的时间(用户设置了一个30分钟的定时器,然后不小心关闭了页面,应该如何?)
运行时的配置,因为我们不能把数据库连接的信息硬编码到代码里。
日志。日志对于Web应用的作用不言而喻。
2.我用到了哪些库?
路由和Web服务器:Spock
最终我选择了Spock,因为它易于使用。如果你用过Ruby的Sinatra,那么Spock应该不会陌生。Spock也自带了会话管理,这一点非常好。
例如,定义一个拥有几条路由的服务器,返回html和JSON,代码大致如下:
{-# LANGUAGE OverloadedStrings #-}
import Web.Spock as Spock
import Web.Spock.Config as Spock
import Data.Aeson as A
main :: IO
main = do
spockCfg <- defaultSpockCfg PCNoDatabase
runSpock 3000 $ spock spockCfg $ do
get root $ do
Spock.html "<div>Hello world!</div>"
get "users" $ do
Spock.json (A.object [ "users" .= users ])
get ("users" <//> var <//> "friends") $ \userID -> do
Spock.json (A.object [ "userID" .= (userID :: Int), "friends" .= A. ])
where users :: [String]
users = ["bob", "alice"]
数据库访问:postgresql-simple
postgresql-simple基本上只允许您对数据库运行原始SQL查询,而没有多余的装饰,例如防止注入攻击。它可以实现您的期望,仅此而已。
{-# LANGUAGE OverloadedStrings #-}
import Database.PostgreSQL.Simple
userLoginsQuery :: Query
userLoginsQuery =
"SELECT l.user_id, COUNT(1) FROM logins l GROUP BY l.user_id;"
getUserLogins :: Connection -> IO [(Int, Int)]
getUserLogins conn = query_ conn userLoginsQuery
数据库访问:postgresql-simple
postgresql-simple可以让你在数据库上运行原始的SQL查询,它只提供最基本的额外处理,比如防止注入攻击等。它仅完成你需要的东西,没有任何额外的功能。
{-# LANGUAGE OverloadedStrings #-}
import Database.PostgreSQL.Simple
userLoginsQuery :: Query
userLoginsQuery =
"SELECT l.user_id, COUNT(1) FROM logins l GROUP BY l.user_id;"
getUserLogins :: Connection -> IO [(Int, Int)]
getUserLogins conn = query_ conn userLoginsQuery
配置:configurator
configurator能够从文件中读取配置,并解析成Haskell数据类型。与普通的配置文件读取器相比它的功能要多一些。
如果你习惯了直接读取配置文件,那么configurator还有一些额外功能。例如,配置项可以嵌套分组,configurator还提供了热重载来监视配置文件变化。
# An example config file.
App_name = "The Whispering Fog"
db {
pool {
stripes = 4
resource_ttl = 300
}
username = "pallas"
password = "thefalloflatinium"
dbname = "italy"
}
{-# LANGUAGE OverloadedStrings #-}
import Data.Configurator as Cfg
import Database.PostgreSQL.Simple
data MyAppConfig = MyAppConfig
{ appName :: String
, appDBConnection :: Connection
}
getAppConfig :: IO MyAppConfig
getAppConfig = do
cfgFile <- Cfg.load ["app-configuration.cfg"]
name <- Cfg.require cfgFile "app_name"
conn <- do
username <- Cfg.require cfgFile "db.username"
password <- Cfg.require cfgFile "db.password"
dbname <- Cfg.require cfgFile "db.dbname"
connect $ defaultConnectInfo
{ connectUser = username
, connectPassword = password
, connectDatabase = dbname
}
pure $ MyAppConfig
{ appName = name
, appDBConnection = conn
}
日志:fast-logger
fast-logger提供了一个相对简单易用的日志解决方案。在Web应用程序的示例中,我只输出到了stderr,但它还可以支持输出日志到文件。虽然它支持许多类型,但绝大多数情况下,你需要定义一个辅助函数,接收一个LoggerSet,以及需要记录的信息。
import System.Log.FastLogger as Log
logMsg :: Log.LoggerSet -> String -> IO
logMsg logSet msg =
Log.pushLogStrLn logSet (Log.toLogStr msg)
doSomething :: IO
doSomething = do
logSet <- Log.newStderrLoggerSet Log.defaultBufSize
logMsg logSet "message 1"
logMsg logSet "message 2"
HTML生成:blaze-html
尽管本项目的后台并不需要生成太多HTML,但值得一提的是,blaze-html正是我需要的。
基本上它就是将HTML浅层嵌入到了Haskell DSL中。如果你会编写HTML,那你就会使用这个库。
{-# LANGUAGE OverloadedStrings #-}
import Data.ByteString.Lazy
import Text.Blaze.Html5 as HTML
import Text.Blaze.Html5.Attributes as HTML hiding ( title )
import Text.Blaze.Html.Renderer.Utf8 as HTML
dashboardHTML :: HTML.Html
dashboardHTML = HTML.html $
HTML.docTypeHtml $ do
HTML.head $ do
HTML.title "Timers and Notes"
HTML.meta ! HTML.charset "utf-8"
HTML.script ! HTML.src "/js/bundle.js" $ ""
HTML.body $ do
HTML.div ! HTML.id "content" $ ""
dashboardBytes :: ByteString
dashboardBytes = HTML.renderHtml dashboardHTML
构建前端:make + npm
没错,它们并不是库。但我们依然需要某种JAVAScript前端,因为定时器需要实时更新。Webpack能够生成JS包,而Make能够组装最终的应用程序。
这些东西无需我多说,网上有很多相关的资源。
3.我必须要用这些库吗?
当然不是。如果你第一次接触Haskell,那么有这些疑问是很自然的。不要让这篇文章限制了你的思路。尽管这个应用程序可以运行,但许多部分用于生产环境下的Haskell时并不理想。
例如,许多Haskell程序员很可能会使用Servant而不是Spock来定义API端点。如果你想了解其他库,那当然应该跟随你的直觉。
你可以把这些库和这个应用程序作为起点。我建议你用这些代码作为学习的机会,理解它的原理,然后自己试着修改。Haskell很好的一点就是它非常易于重构或者升级,而不会破坏已有的功能。
一旦掌握了这个应用程序,就可以用更高级的库来替换它们,来获得更多的保证。同时,这也是一个增量学习的过程。
将数据库访问的库从postgresql-simple升级到支持类型安全的库。我推荐Opaleye!
将API定义的库从Spock升级到Servant
利用QuickCheck或hedgehog增加自动测试。例如,你可以测试服务器的每个错误响应都返回了JSON格式的错误信息。
你还可以尝试替换前端和构建系统。
升级前端,使用PureScript或Elm来替换原始的JavaScript
升级构建系统,利用Shake替换make构建更健壮的系统
原文:https://williamyaoh.com/posts/2019-11-16-a-dead-simple-web-stack.html
本文为 CSDN 翻译,转载请注明来源出处。
【End】