Ужасно простой веб стек на Haskell
В Haskell hasесть aбольшой proliferationвыбор ofраспространненных librariesбиблиотек toдля chooseвсех fromпростых forнужд, allот ofлогирования yourдоступа basicв backendбазу needs,данных fromдо loggingмаршрутизации toи databaseподъема accessвеб-сервера. toВсегда routingхорошо andиметь webсвободу serverвыбора, definition.но It’sесли niceвы toпросто haveначинаете, theколичество freedomрешений ofможет choice,сильно butмешашть. ifМожет you’reбыть justвы startingдаже out,еще theне amountуверены, ofчто decisionsвы neededспособны canпонять beважное overwhelming.различние Maybeмежду you’reвыборами. notВам evenнужно confidentсделать enoughзапрос yetв thatбд. you’llВам beнужны ableгарантирование toстрогие discernимена importantколонки differencesи between all the choices out there. You need to query your database. Do you want the strong column name guarantees and deepглубокое SQL embeddingвстраивание thatкоторое Squeal givesдает you,вам, orили wouldвы youвозможно preferпредпочтете theотносительную relativeпростоту simplicity ofили Opaleye whileс stillтипобезопасностью? gettingИли, typeможет. safety?лучше Or maybe it would be better to just useиспользовать postgresql-simple andи keepоставить thingsвсё reallyпроще-простого? easy?Или Orчто whatнасчет about usingиспользования Selda? OrИли whatчто about…на счет....
AsВозможность aпоказать wayчто toвам showне thatнужно youпроводить don’tчасы needнасколько toваш spendстек tonsкрут, of- agonizingэто aboutвозможность howнаучиться advancedсамому. yourЯ stackнаписал is,пример asвеб-приложения wellиспользующий asсамые aпростые learningбиблиотеки, opportunityкоторые forя myself,смог I’veнайти. writtenЕсли anвы exampleне webуверенны, applicationкак usingстроить theреальное simplestприложение libraries I could find. If you’re not sure how to go about building real applications inна Haskell, whyпочему notбы tryне learningначать fromс it?этого? I’veЯ deliberatelyумышленно triedпостарался toсоставить keepкодову theбазу codebaseмаксимально as simple as I could. You can find the source code here.простой.
Let’sПройдёмся goпо overбиблиотекам whatкоторые librariesя Iвыбрал choseи andчто whatвы youдолжны shouldожидать expectот fromних, them,ну asи wellпонять asчто whatэто theза application is.приложене.
Okay,Хорошо, soчтоже whatтакие isэто thisваше web app anyways?веб-приложеение?
It’sЭто aсайт, siteгде whereпользователи usersмогут canсоздать createтаймеры customи timers and notes for themselves.записки.
ForНаприрмер, instance,один oneиз useпримеров caseиспользования mightможет beбыть cooking:готовка: SomeoneКому-то mightнужно needнастройки toразличные setтаймеры upдля variousотслеживания differentпрогресса timersразличных toреагентов, trackили theсоставить progressзаметки ofо differentвещах reagents,о asкоторых wellнеобходимо asбеспокоиться, notesвещи toкоторые themselvesмогут aboutбыть thingsулучшены thatв theyследующий haveраз toповторя beрецепт. carefulДругой of,вариант thingsможет thatбыть can be improved for the next time they make a recipe, etc. Another use case might be someone playing aигра MOBA, likeкак LeagueLoL ofили LegendsDota2, orгде Dotaможно 2,открыть whereстраничку theyво couldвтором haveмониторе aдля pageослеживания openкулдаунов, inа aтак secondже monitorзаписи toо trackтом keyкак cooldowns,противостоять asпротивникам wellи asих notesкулдаунам toво themselvesвремя about how to macro versus the enemy composition and cooldowns to keep in mind while teamfighting.битвы.
ThisДавайте letsя me demonstrate:покажу:
Sessions,Сессии,sinceпользователиusersдолжныshouldиметьbeвозможностьableобновитьtoстраницу,refreshилиtheуйтиpage,иorвернуться,leaveаandэлементыcomeдолжнаback,всёandещеstill see the same elements.оставаться.PersistenceПостоянствоandиdatabaseдоступaccess,кsinceбазеweданных,needнамtoнужноsaveхранитьtheтаймерыtimersиandзапискиnotesдляforкаждогоeachпользователя.user.ЕщеAnotherтонкость,subtletyэтоisтаймерыthatдолжныtimersзранитьshouldоставшесяretain their remaining time.время. (WhatЧтоifеслиit’s aэто 30minutesминутныйtimerтаймер,andаtheпользовательuserслучайноaccidentallyзакрылcloses the page?вкладку?)Run-timeНастройкаconfiguration,воsinceвремяweработы,can’tтакhardcodeкакdatabaseмыconnectionнеinfo.можем хардкодить информацию о подключении к бд.Logging.Логирование.Self-explanatoryСамо-собойforразумеющеесяaдляweb application.веб-приложения.
Here’sИсходный theкод application source codeприложения.
Okay,Что soэто whatза are the libraries?библиотеки?
RoutingМаршрутизация and web server:веб-сверера: Spock
Spock ends- upиз-за beingпростоты theиспользования. libraryЕсли ofвы choiceкогда-либо hereпользовали becauseSinatra of its ease of use. If you’re ever used Ruby’s Sinatra,Ruby, Spock shouldдолжен feelбыть veryочень similar.1похож. SpockОн alsoтак comesже withидеть sessionс handlingобработкой outсессии ofиз theкоробки, box,что whichочень is very nice.здорово.
ForДля instance,примера, definingопределим aсервер serverс withнесколькими aмаршрутами fewдля routesобратной to send backотправки HTML andи JSONJson mightможет lookвыглядить likeследующим this:образом:
{-# 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.Null ])
where users :: [String]
users = ["bob", "alice"]
DatabaseДоступ access:к базе данных: postgresql-simple
postgresql-simple basically- justпросто letsпозволяет youвам run rawзапустить SQL queriesзапрос againstк yourвашей database,базе withданных, aс minimumминимумом ofдополнительных extraизлишеств, frills,таких suchкак asзащита protectionпротив againstinjection-аттак. injectionОн attacks.просто Itделает, doesчто whatвам youнужно, wouldне expect and nothing more.больше.
{-# 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
Configuration:Настройки: configurator
configurator readsчитает configurationконфигурационные filesфайлы andи parsesпарсит themих intoв Haskellтипы datatypes.данных It’sHaskell. aНемного bitбольше moreчем featurefulпросто thanобычная yourчиталка usualфайлом configконфига. fileимеет reader;несколько ifтрюков you’reв usedрукаве. toКонфигурационные flatатрибуты configurationмогут files,быть configuratorвложенными hasдля aгруппировки, fewтак moreже tricksконфигуратор upпредоставляет itsбыструю sleeves.перезагрузку Configпри attributesизменении canнастроек beконфига, nestedесли forэто grouping, and configurator also provides hot reloading on config file change, if you need that.нужно.
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
}
Logging:Логирование: fast-logger
fast-logger provides- aпредоставляет reasonablyразумно simple-to-useпростое loggingв solution.использовании Inсредство theлогирования. exampleВ webпримере application,веб Iприложения justя useпросто itиспользую toдля printвывода tostderr
, stderr,но butу itбиблиотеки hasесть optionsвозможность toдля logлогирования toв filesфайлы asв well.том Whileчисле. itТак hasкак aесть lotмножество ofтипов, types,в forбольшинстве theвы mostзахотите partопределить you’llфункции-помошники wantкоторые toпросто defineпринимают helperLoggerSet
functionsи thatсообщение justкоторое takeнужно in a LoggerSet and the message you want to log.записать.
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"
GeneratingГенерация HTML: blaze-html
WhileТак thereкак wasn’tу muchнас HTMLне thatбудет neededбольшого toколичества beHTML, generatedкоторое fromнужно theбудет backendгенерировать onв thisэтом project,проекте, it’sстоит worth mentioningупомянуть blaze-html for the parts that I did need.
It’sЭто essentiallyестественно justпросто anмелкое shallow embedding ofвстраивание HTML into aв Haskell DSL. IfЕсли youвы canможете writeнаписать HTML, youвы alreadyуже knowзнаете howкак toиспользовать use this library.библиотеку.
{-# 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
BuildingСборка andи frontend:фротентд: make + npm
Yeah,Да yeah,да, theseэто aren’tне libraries.библиотеки. Still,Но weвсё neededже, someнам sortнужно ofчто-то похожее на JavaScript frontend,фронтенд, sinceтак theкак timersтаймеры haveдолжны toобновляться updateв inреальном realtime.времени. Webpack produced theсоздает JS bundle,пакет, whileв то время как Make assembledсобирает theрезультат final application output.приложения.
IЯ won’tне talkхочу tooоб muchэтом aboutмного these.говорить. ThereЕсть areмножество plentyисточников ofпро resourcesиспользование aboutи usingтого bothи ofдругого these tools elsewhere.инструмента.
DoМне Iнужно haveэто to use these?использовать?
No,Нет, ofконечно courseже not.нет. IfЕсли you’reвы exploringисследуете Haskell inизначально, theто firstвам place,возможно you’reинтересно. probablyНе naturallyпозволяйте curious.мне Don’tвас letудержать meили holdдиктовать youчто downвы orдолжны dictateделать. whatПока youэто shouldприложение do.работает, Whileмногие thisчасти applicationего works,могут manyсчитаться partsнеидиоматическими ofдля it would be considered unidiomatic for productionпроизводства Haskell. ForДля instance,примера, manyмножество Haskellersхаскеллят, wouldскорей likelyвсего, useбудут использовать Servant
insteadвместо ofSpock
Spockдля for defining theсоздания API endpoints.точек Ifдоступа. you’reЕсли interestedвы inзаинтересовались otherеще libraryчем-то, choices,то youдолжны shouldследовать absolutely follow your inclinations.дальше.
ThinkСчитайте ofэти theseбиблиотеки librariesи andэто thisприложение applicationкак asточку aотсчёта. startingЯ point.прошу Iвас encourageиспользовать youэтот toкод useкак thisвозможность codeизучить asи aпонять learningкак opportunityи andчто figureработает, outзатем howначать itмастерить. works,Одна thenиз startпрекраснейших tinkering.вещей One of the beautiful things aboutпро Haskell isэто howто, easyнасколько itпросто isперерабатывать toили refactorобновлятся orбез upgradeпроблем withoutчто-то breakingсломать. things.Как Onceтолько you’veвы gotсделаете aэто handleприложение, onпочему thisбы application,не whyзаменить notчасти tryна replacingболее partsинтересные ofбиблиотеки itкоторые withдают moreвам advancedбольше librariesгарантий. thatкак giveпусть youпостеменного more guarantees, as a way of incrementally learningизучения Haskell?
-
Upgrade the DB access to use a type-safe query library instead of postgresql-simple. I recommend Opaleye!
-
Upgrade the API definition to use Servant instead of Spock.
-
Add automated testing using QuickCheck or hedgehog. For instance, you could test the property that every error response from the server also sends back a JSON error message. And you could even try replacing the frontend and build system.
-
Upgrade the frontend code to use PureScript or Elm instead of vanilla JavaScript.
-
Upgrade the build system to use Shake instead of Make to make things more robust.