From fe848b9e98c994b3bb572ff45da3b835ffffd1b2 Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Sun, 1 Feb 2026 18:56:54 +0100 Subject: [PATCH 01/29] Fix typos in "Persistent" chapter --- book/asciidoc/persistent.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/book/asciidoc/persistent.asciidoc b/book/asciidoc/persistent.asciidoc index 1a3f60d..540b3da 100644 --- a/book/asciidoc/persistent.asciidoc +++ b/book/asciidoc/persistent.asciidoc @@ -1715,5 +1715,5 @@ Persistent integrates directly into the general Yesod workflow. Not only do helper packages like +yesod-persistent+ provide a nice layer, but packages like +yesod-form+ and +yesod-auth+ also leverage Persistent's features as well. -For more information on the syntax of entity declarations, database connection, etc. -Checkout https://github.com/yesodweb/persistent/tree/master/docs +For more information on the syntax of entity declarations, database connection, +etc., check out https://github.com/yesodweb/persistent/tree/master/docs. From 3ef9f98bd001d653d83c9e8acb185c8a647950a5 Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Tue, 3 Feb 2026 13:29:41 +0100 Subject: [PATCH 02/29] Unify spelling of "JavaScript" in the book --- book/asciidoc/blog-example-advanced.asciidoc | 2 +- book/asciidoc/deploying-your-webapp.asciidoc | 4 ++-- book/asciidoc/forms.asciidoc | 8 +++---- book/asciidoc/routing-and-handlers.asciidoc | 2 +- ...scaffolding-and-the-site-template.asciidoc | 8 +++---- .../asciidoc/shakespearean-templates.asciidoc | 2 +- book/asciidoc/widgets.asciidoc | 2 +- book/asciidoc/wiki-chat-example.asciidoc | 8 +++---- book/asciidoc/yesod-for-haskellers.asciidoc | 22 +++++++++---------- book/asciidoc/yesods-monads.asciidoc | 4 ++-- 10 files changed, 31 insertions(+), 31 deletions(-) diff --git a/book/asciidoc/blog-example-advanced.asciidoc b/book/asciidoc/blog-example-advanced.asciidoc index 9e9cf4b..7775d11 100644 --- a/book/asciidoc/blog-example-advanced.asciidoc +++ b/book/asciidoc/blog-example-advanced.asciidoc @@ -252,7 +252,7 @@ good idea to always check for pending messages in your defaultLayout function. mmsg <- getMessage ---- -We use widgets to compose together HTML, CSS and Javascript. At the end of the +We use widgets to compose together HTML, CSS and JavaScript. At the end of the day, we need to unwrap all of that into simple HTML. That's what the +widgetToPageContent+ function is for. We're going to give it a widget consisting of the content we received from the individual page (inside), plus a standard diff --git a/book/asciidoc/deploying-your-webapp.asciidoc b/book/asciidoc/deploying-your-webapp.asciidoc index aeac91c..ba8d1fb 100644 --- a/book/asciidoc/deploying-your-webapp.asciidoc +++ b/book/asciidoc/deploying-your-webapp.asciidoc @@ -93,7 +93,7 @@ out a safe storage solution for the client session key. There are two commonly used features in the Yesod world: serving your site over HTTPS, and placing your static files on a separate domain name. While both of these are good practices, when combined they can lead to problems if you're not -careful. In particular, most web browsers will not load up Javascript files +careful. In particular, most web browsers will not load up JavaScript files from a non-HTTPS domain name if your HTML is served from an HTTPS domain name. In this situation, you'll need to do one of two things: @@ -344,7 +344,7 @@ A similar approach, without requiring the QtWebkit library, is wai-handler-launch, which launches a Warp server and then opens up the user's default web browser. There's a little trickery involved here: in order to know that the user is still using the site, +wai-handler-launch+ inserts a "ping" -Javascript snippet to every HTML page it serves. It +wai-handler-launch+ +JavaScript snippet to every HTML page it serves. It +wai-handler-launch+ doesn't receive a ping for two minutes, it shuts down. === CGI on Apache diff --git a/book/asciidoc/forms.asciidoc b/book/asciidoc/forms.asciidoc index ea1b8f6..993a20e 100644 --- a/book/asciidoc/forms.asciidoc +++ b/book/asciidoc/forms.asciidoc @@ -11,7 +11,7 @@ solution that addresses the following problems: * Generate HTML code for displaying the form. -* Generate Javascript to do clientside validation and provide more +* Generate JavaScript to do clientside validation and provide more user-friendly widgets, such as date pickers. * Build up more complex forms by combining together simpler forms. @@ -20,7 +20,7 @@ solution that addresses the following problems: The yesod-form package provides all these features in a simple, declarative API. It builds on top of Yesod's widgets to simplify styling of forms and -applying Javascript appropriately. And like the rest of Yesod, it uses +applying JavaScript appropriately. And like the rest of Yesod, it uses Haskell's type system to make sure everything is working correctly. === Synopsis @@ -663,7 +663,7 @@ monadic form with an applicative one and the code would still work. Applicative and monadic forms handle both the generation of your HTML code and the parsing of user input. Sometimes, you only want to do the latter, such as when there's an already-existing form in HTML somewhere, or if you want to -generate a form dynamically using Javascript. In such a case, you'll want input +generate a form dynamically using JavaScript. In such a case, you'll want input forms. These work mostly the same as applicative and monadic forms, with some differences: @@ -928,7 +928,7 @@ whether the field is required or optional. The result is six helper functions: +areq+, +aopt+, +mreq+, +mopt+, +ireq+, and +iopt+. Forms have significant power available. They can automatically insert -Javascript to help you leverage nicer UI controls, such as a jQuery UI date +JavaScript to help you leverage nicer UI controls, such as a jQuery UI date picker. Forms are also fully i18n-ready, so you can support a global community of users. And when you have more specific needs, you can slap on some validation functions to an existing field, or write a new one from scratch. diff --git a/book/asciidoc/routing-and-handlers.asciidoc b/book/asciidoc/routing-and-handlers.asciidoc index 4ee33d3..ecf5e04 100644 --- a/book/asciidoc/routing-and-handlers.asciidoc +++ b/book/asciidoc/routing-and-handlers.asciidoc @@ -353,7 +353,7 @@ should remember that the function can be used in your +Handler+ functions. There's nothing too surprising about this type. This function returns some HTML content, represented by the +Html+ data type. But clearly Yesod would not be useful if it only allowed HTML responses to be generated. We want to respond with -CSS, Javascript, JSON, images, and more. So the question is: what data types +CSS, JavaScript, JSON, images, and more. So the question is: what data types can be returned? In order to generate a response, we need to know two pieces of information: diff --git a/book/asciidoc/scaffolding-and-the-site-template.asciidoc b/book/asciidoc/scaffolding-and-the-site-template.asciidoc index 55062e5..d8389c4 100644 --- a/book/asciidoc/scaffolding-and-the-site-template.asciidoc +++ b/book/asciidoc/scaffolding-and-the-site-template.asciidoc @@ -210,7 +210,7 @@ You can use the +stack exec \-- yesod add-handler+ command to automate the last === widgetFile -It's very common to want to include CSS and Javascript specific to a page. You +It's very common to want to include CSS and JavaScript specific to a page. You don't want to have to remember to include those Lucius and Julius files manually every time you refer to a Hamlet file. For this, the site template provides the +widgetFile+ function. @@ -280,16 +280,16 @@ transmission of cookies for static file requests, and also lets you offload static file hosting to a CDN or a service like Amazon S3. See the comments in the file for more details. -Another optimization is that CSS and Javascript included in your widgets will +Another optimization is that CSS and JavaScript included in your widgets will not be included inside your HTML. Instead, their contents will be written to an external file, and a link given. This file will be named based on a hash of the contents as well, meaning: . Caching works properly. -. Yesod can avoid an expensive disk write of the CSS/Javascript file contents if a file with the same hash already exists. +. Yesod can avoid an expensive disk write of the CSS/JavaScript file contents if a file with the same hash already exists. -Finally, all of your Javascript is automatically minified via hjsmin. +Finally, all of your JavaScript is automatically minified via hjsmin. === Environment variables diff --git a/book/asciidoc/shakespearean-templates.asciidoc b/book/asciidoc/shakespearean-templates.asciidoc index a452462..81f1548 100644 --- a/book/asciidoc/shakespearean-templates.asciidoc +++ b/book/asciidoc/shakespearean-templates.asciidoc @@ -24,7 +24,7 @@ them to enhance Yesod application development. === Synopsis There are four main languages at play: Hamlet is an HTML templating language, -Julius is for Javascript, and Cassius and Lucius are both for CSS. Hamlet and +Julius is for JavaScript, and Cassius and Lucius are both for CSS. Hamlet and Cassius are both whitespace-sensitive formats, using indentation to denote nesting. By contrast, Lucius is a superset of CSS, keeping CSS's braces for denoting nesting. Julius is a simple passthrough language for producing diff --git a/book/asciidoc/widgets.asciidoc b/book/asciidoc/widgets.asciidoc index 808602d..093b5d8 100644 --- a/book/asciidoc/widgets.asciidoc +++ b/book/asciidoc/widgets.asciidoc @@ -116,7 +116,7 @@ page. In particular: * The title * External stylesheets -* External Javascript +* External JavaScript * CSS declarations * JavaScript code * Arbitrary ++ content diff --git a/book/asciidoc/wiki-chat-example.asciidoc b/book/asciidoc/wiki-chat-example.asciidoc index ffc7435..79626ea 100644 --- a/book/asciidoc/wiki-chat-example.asciidoc +++ b/book/asciidoc/wiki-chat-example.asciidoc @@ -195,12 +195,12 @@ is mostly identical for any subsite you'll write. We now have a fully working subsite. The final component we want as part of our chat library is a widget to be embedded inside a page which will provide chat functionality. By creating this as a widget, we can include all of our HTML, -CSS, and Javascript as a reusable component. +CSS, and JavaScript as a reusable component. Our widget will need to take in one argument: a function to convert a +Chat+ subsite URL into a master site URL. The reasoning here is that an application developer could place the chat subsite anywhere in the URL structure, and this -widget needs to be able to generate Javascript which will point at the correct +widget needs to be able to generate JavaScript which will point at the correct URLs. Let's start off our widget: [source, haskell] @@ -235,7 +235,7 @@ function lives in the +Handler+ monad, we need to use +handlerToWidget+: ---- If the user is logged in, we want to display the chat box, style it with some -CSS, and then make it interactive using some Javascript. This is mostly +CSS, and then make it interactive using some JavaScript. This is mostly client-side code wrapped in a Widget: [source, haskell] @@ -264,7 +264,7 @@ client-side code wrapped in a Widget: overflow: auto; } |] - -- And now that Javascript + -- And now that JavaScript toWidgetBody [julius| // Set up the receiving end var output = document.getElementById(#{toJSON output}); diff --git a/book/asciidoc/yesod-for-haskellers.asciidoc b/book/asciidoc/yesod-for-haskellers.asciidoc index 86e0ae2..bc124b0 100644 --- a/book/asciidoc/yesod-for-haskellers.asciidoc +++ b/book/asciidoc/yesod-for-haskellers.asciidoc @@ -278,7 +278,7 @@ Yesod has a concept of a *foundation data type*. This is a data type at the core of each application, and is used in three important ways: * It can hold onto values that are initialized and shared amongst all aspects of your application, such as an HTTP connection manager, a database connection pool, settings loaded from a file, or some shared mutable state like a counter or cache. -* Typeclass instances provide even more information about your application. The +Yesod+ typeclass has various settings, such as what the default template of your app should be, or the maximum allowed request body size. The +YesodDispatch+ class indicates how incoming requests should be dispatched to handler functions. And there are a number of typeclasses commonly used in Yesod helper libraries, such as +RenderMessage+ for i18n support or +YesodJquery+ for providing the shared location of the jQuery Javascript library. +* Typeclass instances provide even more information about your application. The +Yesod+ typeclass has various settings, such as what the default template of your app should be, or the maximum allowed request body size. The +YesodDispatch+ class indicates how incoming requests should be dispatched to handler functions. And there are a number of typeclasses commonly used in Yesod helper libraries, such as +RenderMessage+ for i18n support or +YesodJquery+ for providing the shared location of the jQuery JavaScript library. * Associated types (i.e., type families) are used to create a related *route data type* for each application. This is a simple ADT that represents all legal routes in your application. But using this intermediate data type instead of dealing directly with strings, Yesod applications can take advantage of the compiler to prevent creating invalid links. This feature is known as *type safe URLs*. In keeping with the spirit of this chapter, we're going to create our first @@ -574,7 +574,7 @@ Some requests to a web application can be displayed with various *representation * An HTML table * A CSV file * XML -* JSON data to be consumed by some client-side Javascript +* JSON data to be consumed by some client-side JavaScript The HTTP spec allows a client to specify its preference of representation via the +accept+ request header. And Yesod allows a handler function to use the @@ -985,7 +985,7 @@ piece of checking that you have to perform. While generating plain text pages can be fun, it's hardly what one normally expects from a web framework. As you'd hope, Yesod comes built in with support -for generating HTML, CSS and Javascript as well. +for generating HTML, CSS and JavaScript as well. Before we get into templating languages, let's do it the raw, low-level way, and then build up to something a bit more pleasant. @@ -1008,7 +1008,7 @@ getStyleR = return $ TypedContent typeCss $ toContent getScriptR :: LiteHandler TypedContent getScriptR = return $ TypedContent typeJavascript $ toContent - "alert('Yay, Javascript works too!');" + "alert('Yay, JavaScript works too!');" main :: IO () main = warp 3000 $ liteApp $ do @@ -1018,7 +1018,7 @@ main = warp 3000 $ liteApp $ do ---- We're just reusing all of the +TypedContent+ stuff we've already learnt. We now -have three separate routes, providing HTML, CSS and Javascript. We write our +have three separate routes, providing HTML, CSS and JavaScript. We write our content as ++String++s, convert them to +Content+ using +toContent+, then wrap them with a +TypedContent+ constructor to give them the appropriate content-type headers. @@ -1051,7 +1051,7 @@ getStyleR = return $ TypedContent typeCss $ toContent getScriptR :: LiteHandler TypedContent getScriptR = return $ TypedContent typeJavascript $ toContent - "alert('Yay, Javascript works too!');" + "alert('Yay, JavaScript works too!');" main :: IO () main = warp 3000 $ liteApp $ do @@ -1099,7 +1099,7 @@ getStyleR :: LiteHandler Css getStyleR = withUrlRenderer [lucius|h1 { color: red }|] getScriptR :: LiteHandler Javascript -getScriptR = withUrlRenderer [julius|alert('Yay, Javascript works too!');|] +getScriptR = withUrlRenderer [julius|alert('Yay, JavaScript works too!');|] main :: IO () main = warp 3000 $ liteApp $ do @@ -1112,7 +1112,7 @@ main = warp 3000 $ liteApp $ do Likely the most confusing part of this is the +withUrlRenderer+ calls. This gets into one of the most powerful features of Yesod: type-safe URLs. If you -notice in our HTML, we're providing links to the CSS and Javascript URLs via +notice in our HTML, we're providing links to the CSS and JavaScript URLs via strings. This leads to a duplication of that information, as in our +main+ function we have to provide those strings a second time. This is very fragile: our codebase is one refactor away from having broken links. @@ -1133,7 +1133,7 @@ since Widgets end up providing the renderer function for us automatically. === Widgets -Dealing with HTML, CSS and Javascript as individual components can be nice in +Dealing with HTML, CSS and JavaScript as individual components can be nice in many cases. However, when you want to build up reusable components for a page, it can get in the way of composability. If you want more motivation for why widgets are useful, please see the widget chapter. For now, let's just dig into @@ -1149,14 +1149,14 @@ getHomeR = defaultLayout $ do setTitle $ toHtml "Hi There!" [whamlet|

Hello World!|] toWidget [lucius|h1 { color: red }|] - toWidget [julius|alert('Yay, Javascript works too!');|] + toWidget [julius|alert('Yay, JavaScript works too!');|] main :: IO () main = warp 3000 $ liteApp $ dispatchTo getHomeR ---- This is the same example as above, but we've now condensed it into a single -handler. Yesod will automatically handle providing the CSS and Javascript to +handler. Yesod will automatically handle providing the CSS and JavaScript to the HTML. By default, it will place them in +style+ and +script+ tags in the +head+ and +body+ of the page, respectively, but Yesod provides many customization settings to do other things (such as automatically creating diff --git a/book/asciidoc/yesods-monads.asciidoc b/book/asciidoc/yesods-monads.asciidoc index e00718e..6290cf1 100644 --- a/book/asciidoc/yesods-monads.asciidoc +++ b/book/asciidoc/yesods-monads.asciidoc @@ -4,7 +4,7 @@ As you've read through this book, there have been a number of monads which have appeared: +Handler+, +Widget+ and +YesodDB+ (for Persistent). As with most monads, each one provides some specific functionality: +Handler+ gives access to the request and allows you to send responses, a +Widget+ contains HTML, CSS, -and Javascript, and +YesodDB+ lets you make database queries. In +and JavaScript, and +YesodDB+ lets you make database queries. In Model-View-Controller (MVC) terms, we could consider +YesodDB+ to be the model, +Widget+ to be the view, and +Handler+ to be the controller. @@ -112,7 +112,7 @@ together, we can now run database actions inside our handlers. Most of the time in Yesod code, and especially thus far in this book, widgets have been treated as actionless containers that simply combine together HTML, -CSS and Javascript. But in reality, a +Widget+ can do anything that a +Handler+ +CSS and JavaScript. But in reality, a +Widget+ can do anything that a +Handler+ can do, by using the +handlerToWidget+ function. So for example, you can run database queries inside a +Widget+ by using something like +handlerToWidget . runDB+. From 973945d181a5c7c8d9821efe38ef1256830370b3 Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Tue, 3 Feb 2026 13:36:11 +0100 Subject: [PATCH 03/29] Reflect deprecation of `persistent-template` --- book/asciidoc/persistent.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/asciidoc/persistent.asciidoc b/book/asciidoc/persistent.asciidoc index 540b3da..0906881 100644 --- a/book/asciidoc/persistent.asciidoc +++ b/book/asciidoc/persistent.asciidoc @@ -47,7 +47,7 @@ Persistent on its own. === Synopsis -The required dependencies for the below are: persistent, persistent-sqlite and persistent-template. +The required dependencies for the below are: persistent and persistent-sqlite. [source, haskell] From 705f85c7cd65431c0f211863f696735d5ee23322 Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Thu, 12 Feb 2026 22:14:57 +0100 Subject: [PATCH 04/29] Update to new Conduit API --- book/asciidoc/case-study-sphinx.asciidoc | 28 ++++++++++---------- book/asciidoc/json-web-service.asciidoc | 4 +-- book/asciidoc/persistent.asciidoc | 8 +++--- book/asciidoc/sql-joins.asciidoc | 6 ++--- book/asciidoc/understanding-request.asciidoc | 2 +- book/asciidoc/yesod-for-haskellers.asciidoc | 6 ++--- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/book/asciidoc/case-study-sphinx.asciidoc b/book/asciidoc/case-study-sphinx.asciidoc index 79cef6c..21bbba8 100644 --- a/book/asciidoc/case-study-sphinx.asciidoc +++ b/book/asciidoc/case-study-sphinx.asciidoc @@ -373,7 +373,7 @@ to huge memory usage and slow response times. So how exactly do we create a streaming response? Yesod provides a helper function for this case: +responseSourceDB+. This function takes two arguments: -a content type, and a conduit +Source+ providing a stream of blaze-builder +a content type, and a Conduit providing a stream of blaze-builder ++Builder++s. Yesod then handles all of the issues of grabbing a database connection from the connection pool, starting a transaction, and streaming the response to the user. @@ -387,7 +387,7 @@ is: [source, haskell] ---- -renderBuilder :: Monad m => RenderSettings -> Conduit Event m Builder +renderBuilder :: Monad m => RenderSettings -> ConduitT Event Builder m () ---- In plain English, that means +renderBuilder+ takes some settings (we'll just use @@ -482,18 +482,18 @@ the function: [source, haskell] ---- -docSource :: Source (YesodDB Searcher) X.Event -docSource = selectSource [] [] $= CL.concatMap entityToEvents +docSource :: ConduitT () X.Event (YesodDB Searcher) () +docSource = selectSource [] [] .| CL.concatMap entityToEvents ---- -The $= operator joins together a source and a conduit into a new source. Now +The .| operator combines two Conduits together into a new Conduit. Now that we have our +Event+ source, all we need to do is surround it with the -document start and end events. With +Source+'s +Monad+ instance, this is a +document start and end events. With +ConduitT+'s +Monad+ instance, this is a piece of cake: [source, haskell] ---- -fullDocSource :: Source (YesodDB Searcher) X.Event +fullDocSource :: ConduitT () X.Event (YesodDB Searcher) () fullDocSource = do mapM_ yield startEvents docSource @@ -508,8 +508,8 @@ getXmlpipeR :: Handler TypedContent getXmlpipeR = respondSourceDB "text/xml" $ fullDocSource - $= renderBuilder def - $= CL.map Chunk + .| renderBuilder def + .| CL.map Chunk ---- === Full code @@ -719,8 +719,8 @@ getXmlpipeR :: Handler TypedContent getXmlpipeR = respondSourceDB "text/xml" $ fullDocSource - $= renderBuilder def - $= CL.map Chunk + .| renderBuilder def + .| CL.map Chunk entityToEvents :: (Entity Doc) -> [X.Event] entityToEvents (Entity docid doc) = @@ -731,14 +731,14 @@ entityToEvents (Entity docid doc) = , X.EventEndElement document ] -fullDocSource :: Source (YesodDB Searcher) X.Event +fullDocSource :: ConduitT () X.Event (YesodDB Searcher) () fullDocSource = do mapM_ yield startEvents docSource mapM_ yield endEvents -docSource :: Source (YesodDB Searcher) X.Event -docSource = selectSource [] [] $= CL.concatMap entityToEvents +docSource :: ConduitT () X.Event (YesodDB Searcher) () +docSource = selectSource [] [] .| CL.concatMap entityToEvents toName :: Text -> X.Name toName x = X.Name x (Just "http://sphinxsearch.com/") (Just "sphinx") diff --git a/book/asciidoc/json-web-service.asciidoc b/book/asciidoc/json-web-service.asciidoc index 59ac2ef..f202892 100644 --- a/book/asciidoc/json-web-service.asciidoc +++ b/book/asciidoc/json-web-service.asciidoc @@ -22,7 +22,7 @@ import Control.Monad.IO.Class (liftIO) import Data.Aeson (Value, encode, object, (.=)) import Data.Aeson.Parser (json) import Data.ByteString (ByteString) -import Data.Conduit (($$)) +import Data.Conduit (runConduit, (.|)) import Data.Conduit.Attoparsec (sinkParser) import Network.HTTP.Types (status200, status400) import Network.Wai (Application, Response, responseLBS) @@ -34,7 +34,7 @@ main = run 3000 app app :: Application app req sendResponse = handle (sendResponse . invalidJson) $ do - value <- sourceRequestBody req $$ sinkParser json + value <- runConduit $ sourceRequestBody req .| sinkParser json newValue <- liftIO $ modValue value sendResponse $ responseLBS status200 diff --git a/book/asciidoc/persistent.asciidoc b/book/asciidoc/persistent.asciidoc index 0906881..d2b084c 100644 --- a/book/asciidoc/persistent.asciidoc +++ b/book/asciidoc/persistent.asciidoc @@ -733,16 +733,16 @@ All the select functions use a similar interface, with slightly different output [options="header"] |=============== |Function|Returns -|selectSource|A +Source+ containing all the IDs and values from the database. This allows you to write streaming code. +|selectSource|A +ConduitT+ containing all the IDs and values from the database. This allows you to write streaming code. -NOTE: A +Source+ is a stream of data, and is part of the +conduit+ package. I +NOTE: A +ConduitT+ is a monad transformer designed for streaming data, and is part of the +conduit+ package. I recommend reading the link:https://github.com/snoyberg/conduit[Official Conduit tutorial] to get started. |selectList|A list containing all the IDs and values from the database. All records will be loaded into memory. |selectFirst|Takes just the first ID and value from the database, if available -|selectKeys|Returns only the keys, without the values, as a +Source+. +|selectKeys|Returns only the keys, without the values, as a +ConduitT+. |=============== +selectList+ is the most commonly used, so we will cover it specifically. Understanding the others should be trivial after that. @@ -1501,7 +1501,7 @@ main = runSqlite ":memory:" $ do -- Persistent does not provide the LIKE keyword, but we'd like to get the -- whole Snoyman family... let sql = "SELECT name FROM Person WHERE name LIKE '%Snoyman'" - rawQuery sql [] $$ CL.mapM_ (liftIO . print) + runConduit $ rawQuery sql [] .| CL.mapM_ (liftIO . print) ---- There is also higher-level support that allows for automated data marshaling. diff --git a/book/asciidoc/sql-joins.asciidoc b/book/asciidoc/sql-joins.asciidoc index e7e3771..ef59c22 100644 --- a/book/asciidoc/sql-joins.asciidoc +++ b/book/asciidoc/sql-joins.asciidoc @@ -326,7 +326,7 @@ getHomeR = do render <- getUrlRenderParams respondSourceDB typeHtml $ do sendChunkText "Blog posts
    " - blogsSrc $= CL.map (\(E.Value blogid, E.Value title, E.Value name) -> + blogsSrc .| CL.map (\(E.Value blogid, E.Value title, E.Value name) -> toFlushBuilder $ [hamlet|
  • @@ -382,7 +382,7 @@ import Database.Esqueleto ((^.)) import Database.Persist.Sqlite import Yesod import qualified Data.Conduit.List as CL -import Data.Conduit (($=)) +import Data.Conduit ((.|)) share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Author @@ -425,7 +425,7 @@ getHomeR = do render <- getUrlRenderParams respondSourceDB typeHtml $ do sendChunkText "Blog posts
      " - blogsSrc $= CL.map (\(E.Value blogid, E.Value title, E.Value name) -> + blogsSrc .| CL.map (\(E.Value blogid, E.Value title, E.Value name) -> toFlushBuilder $ [hamlet|
    • diff --git a/book/asciidoc/understanding-request.asciidoc b/book/asciidoc/understanding-request.asciidoc index 2cc4261..2374493 100644 --- a/book/asciidoc/understanding-request.asciidoc +++ b/book/asciidoc/understanding-request.asciidoc @@ -99,7 +99,7 @@ but how does Yesod know how to deal with +Html+? The answer lies in the [source, haskell] ---- data Content = ContentBuilder !BBuilder.Builder !(Maybe Int) -- ^ The content and optional content length. - | ContentSource !(Source (ResourceT IO) (Flush BBuilder.Builder)) + | ContentSource !(ConduitT () (Flush BBuilder.Builder) (ResourceT IO) ()) | ContentFile !FilePath !(Maybe FilePart) | ContentDontEvaluate !Content data TypedContent = TypedContent !ContentType !Content diff --git a/book/asciidoc/yesod-for-haskellers.asciidoc b/book/asciidoc/yesod-for-haskellers.asciidoc index bc124b0..200399f 100644 --- a/book/asciidoc/yesod-for-haskellers.asciidoc +++ b/book/asciidoc/yesod-for-haskellers.asciidoc @@ -514,7 +514,7 @@ At the very core of Yesod's content system are the following types: [source, haskell] ---- data Content = ContentBuilder !Builder !(Maybe Int) -- ^ The content and optional content length. - | ContentSource !(Source (ResourceT IO) (Flush Builder)) + | ContentSource !(ConduitT () (Flush Builder) (ResourceT IO) ()) | ContentFile !FilePath !(Maybe FilePart) | ContentDontEvaluate !Content @@ -734,9 +734,9 @@ When we encountered this issue in WAI, we introduced the +responseSource+ method of constructing a response. Using +sendWaiResponse+, we could reuse that same method for creating a streaming response in Yesod. But there's also a simpler API for doing this: +respondSource+. +respondSource+ takes two -parameters: the content type of the response, and a +Source+ of +Flush +parameters: the content type of the response, and a +ConduitT+ that produces a stream of +Flush Builder+. Yesod also provides a number of convenience functions for creating -that +Source+, such as +sendChunk+, +sendChunkBS+, and +sendChunkText+. +that +ConduitT+, such as +sendChunk+, +sendChunkBS+, and +sendChunkText+. Here's an example, which just converts our initial +responseSource+ example from WAI to Yesod. From ec9e68b61b09f0ca60181e45d150b14ff7ee125f Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Thu, 12 Feb 2026 23:31:13 +0100 Subject: [PATCH 05/29] Remove redundant imports --- .../authentication-and-authorization.asciidoc | 3 --- book/asciidoc/blog-example-advanced.asciidoc | 1 - book/asciidoc/case-study-sphinx.asciidoc | 3 --- book/asciidoc/forms.asciidoc | 14 +++++--------- book/asciidoc/json-web-service.asciidoc | 1 - book/asciidoc/persistent.asciidoc | 15 --------------- book/asciidoc/sessions.asciidoc | 2 -- book/asciidoc/shakespearean-templates.asciidoc | 2 +- book/asciidoc/sql-joins.asciidoc | 2 -- book/asciidoc/web-application-interface.asciidoc | 1 - book/asciidoc/wiki-chat-example.asciidoc | 1 - book/asciidoc/yesod-for-haskellers.asciidoc | 8 +------- book/asciidoc/yesod-typeclass.asciidoc | 1 - book/asciidoc/yesods-monads.asciidoc | 4 ++-- 14 files changed, 9 insertions(+), 49 deletions(-) diff --git a/book/asciidoc/authentication-and-authorization.asciidoc b/book/asciidoc/authentication-and-authorization.asciidoc index 126b7c5..739466a 100644 --- a/book/asciidoc/authentication-and-authorization.asciidoc +++ b/book/asciidoc/authentication-and-authorization.asciidoc @@ -79,7 +79,6 @@ Let's jump right in with an example of authentication. For the Google OAuth auth {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -import Data.Default (def) import Data.Text (Text) import Network.HTTP.Client.Conduit (Manager, newManager) import Yesod @@ -258,7 +257,6 @@ import Database.Persist.Sqlite import Database.Persist.TH import Network.Mail.Mime import Text.Blaze.Html.Renderer.Utf8 (renderHtml) -import Text.Hamlet (shamlet) import Text.Shakespeare.Text (stext) import Yesod import Yesod.Auth @@ -422,7 +420,6 @@ your Yesod typeclass instance. Let's see an example. {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -import Data.Default (def) import Data.Text (Text) import Network.HTTP.Conduit (Manager, newManager, tlsManagerSettings) import Yesod diff --git a/book/asciidoc/blog-example-advanced.asciidoc b/book/asciidoc/blog-example-advanced.asciidoc index 7775d11..336e09c 100644 --- a/book/asciidoc/blog-example-advanced.asciidoc +++ b/book/asciidoc/blog-example-advanced.asciidoc @@ -37,7 +37,6 @@ import Database.Persist.Sqlite , createSqlitePool, runSqlPersistMPool ) import Data.Time (UTCTime, getCurrentTime) -import Control.Applicative ((<$>), (<*>), pure) import Data.Typeable (Typeable) import Control.Monad.Logger (runStdoutLoggingT) ---- diff --git a/book/asciidoc/case-study-sphinx.asciidoc b/book/asciidoc/case-study-sphinx.asciidoc index 21bbba8..eedfa92 100644 --- a/book/asciidoc/case-study-sphinx.asciidoc +++ b/book/asciidoc/case-study-sphinx.asciidoc @@ -525,16 +525,13 @@ getXmlpipeR = {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-} -import Control.Applicative ((<$>), (<*>)) import Control.Monad (forM) import Control.Monad.Logger (runStdoutLoggingT) import Data.Conduit import qualified Data.Conduit.List as CL import Data.Maybe (catMaybes) -import Data.Monoid (mconcat) import Data.Text (Text) import qualified Data.Text as T -import Data.Text.Lazy.Encoding (decodeUtf8) import qualified Data.XML.Types as X import Database.Persist.Sqlite import Text.Blaze.Html (preEscapedToHtml) diff --git a/book/asciidoc/forms.asciidoc b/book/asciidoc/forms.asciidoc index 993a20e..426d394 100644 --- a/book/asciidoc/forms.asciidoc +++ b/book/asciidoc/forms.asciidoc @@ -32,9 +32,8 @@ Haskell's type system to make sure everything is working correctly. {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -import Control.Applicative ((<$>), (<*>)) -import Data.Text (Text) -import Data.Time (Day) +import Data.Text (Text) +import Data.Time (Day) import Yesod import Yesod.Form.Jquery @@ -555,8 +554,7 @@ we could code something like this. {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -import Control.Applicative -import Data.Text (Text) +import Data.Text (Text) import Yesod data App = App @@ -693,8 +691,7 @@ you need to correct. With input forms, the user simply gets an error message. {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -import Control.Applicative -import Data.Text (Text) +import Data.Text (Text) import Yesod data App = App @@ -853,8 +850,7 @@ Let's see an example of using these two functions: {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -import Control.Applicative -import Data.Text (Text) +import Data.Text (Text) import Data.Time import Yesod diff --git a/book/asciidoc/json-web-service.asciidoc b/book/asciidoc/json-web-service.asciidoc index f202892..9759fa1 100644 --- a/book/asciidoc/json-web-service.asciidoc +++ b/book/asciidoc/json-web-service.asciidoc @@ -21,7 +21,6 @@ import Control.Exception.Lifted (handle) import Control.Monad.IO.Class (liftIO) import Data.Aeson (Value, encode, object, (.=)) import Data.Aeson.Parser (json) -import Data.ByteString (ByteString) import Data.Conduit (runConduit, (.|)) import Data.Conduit.Attoparsec (sinkParser) import Network.HTTP.Types (status200, status400) diff --git a/book/asciidoc/persistent.asciidoc b/book/asciidoc/persistent.asciidoc index d2b084c..beed58f 100644 --- a/book/asciidoc/persistent.asciidoc +++ b/book/asciidoc/persistent.asciidoc @@ -227,10 +227,8 @@ entities once. Let's see a quick example: {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} -import Database.Persist import Database.Persist.TH import Database.Persist.Sqlite -import Control.Monad.IO.Class (liftIO) mkPersist sqlSettings [persistLowerCase| Person @@ -348,11 +346,9 @@ pass it off to other Persistent functions. {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -import Control.Monad.IO.Class (liftIO) import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH -import Control.Monad.IO.Unlift import Data.Text import Control.Monad.Reader import Control.Monad.Logger @@ -467,14 +463,9 @@ to _ask_ it to help. Let's see what this looks like: {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -import Control.Monad.IO.Class (liftIO) import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH -import Control.Monad.IO.Unlift -import Data.Text -import Control.Monad.Reader -import Control.Monad.Logger import Conduit share [mkPersist sqlSettings, mkEntityDefList "entityDefs", mkMigrate "migrateAll"] [persistLowerCase| @@ -522,7 +513,6 @@ Persistent provides a helper function, +mkMigrate+: {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH @@ -625,7 +615,6 @@ in Haskell as a data constructor. import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH -import Data.Time import Control.Monad.IO.Class (liftIO) share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| @@ -1075,7 +1064,6 @@ favorite programming language: {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Data.Time @@ -1121,7 +1109,6 @@ Salvador: {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Data.Time @@ -1188,7 +1175,6 @@ import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Control.Monad.IO.Class (liftIO) -import Data.Time share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Person @@ -1236,7 +1222,6 @@ want to track which people have shopped in which stores: import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH -import Data.Time share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Person diff --git a/book/asciidoc/sessions.asciidoc b/book/asciidoc/sessions.asciidoc index 2673508..a800e32 100644 --- a/book/asciidoc/sessions.asciidoc +++ b/book/asciidoc/sessions.asciidoc @@ -136,8 +136,6 @@ a value for a key, and +deleteSession+ clears a value for a key. {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE MultiParamTypeClasses #-} -import Control.Applicative ((<$>), (<*>)) -import qualified Web.ClientSession as CS import Yesod data App = App diff --git a/book/asciidoc/shakespearean-templates.asciidoc b/book/asciidoc/shakespearean-templates.asciidoc index 81f1548..f2c0729 100644 --- a/book/asciidoc/shakespearean-templates.asciidoc +++ b/book/asciidoc/shakespearean-templates.asciidoc @@ -467,7 +467,7 @@ example. ---- {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE OverloadedStrings #-} -import Text.Hamlet (HtmlUrl, hamlet) +import Text.Hamlet (hamlet) import Text.Blaze.Html.Renderer.String (renderHtml) import Data.Text (Text, append, pack) import Control.Arrow (second) diff --git a/book/asciidoc/sql-joins.asciidoc b/book/asciidoc/sql-joins.asciidoc index ef59c22..04549f0 100644 --- a/book/asciidoc/sql-joins.asciidoc +++ b/book/asciidoc/sql-joins.asciidoc @@ -40,7 +40,6 @@ the blog title and the author: {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-} -import Control.Monad.Logger import Data.Text (Text) import Database.Persist.Sqlite import Yesod @@ -375,7 +374,6 @@ For completeness, here's the entire body of the final, streaming example: {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-} -import Control.Monad.Logger import Data.Text (Text) import qualified Database.Esqueleto as E import Database.Esqueleto ((^.)) diff --git a/book/asciidoc/web-application-interface.asciidoc b/book/asciidoc/web-application-interface.asciidoc index e19377f..422a068 100644 --- a/book/asciidoc/web-application-interface.asciidoc +++ b/book/asciidoc/web-application-interface.asciidoc @@ -182,7 +182,6 @@ of requests, and then hold that +MVar+ while sending each response. import Blaze.ByteString.Builder (fromByteString) import Blaze.ByteString.Builder.Char.Utf8 (fromShow) import Control.Concurrent.MVar -import Data.Monoid ((<>)) import Network.HTTP.Types (status200) import Network.Wai import Network.Wai.Handler.Warp (run) diff --git a/book/asciidoc/wiki-chat-example.asciidoc b/book/asciidoc/wiki-chat-example.asciidoc index 79626ea..6907c04 100644 --- a/book/asciidoc/wiki-chat-example.asciidoc +++ b/book/asciidoc/wiki-chat-example.asciidoc @@ -25,7 +25,6 @@ module Chat.Data where import Blaze.ByteString.Builder.Char.Utf8 (fromText) import Control.Concurrent.Chan -import Data.Monoid ((<>)) import Data.Text (Text) import Network.Wai.EventSource import Network.Wai.EventSource.EventStream diff --git a/book/asciidoc/yesod-for-haskellers.asciidoc b/book/asciidoc/yesod-for-haskellers.asciidoc index 200399f..6fa3fe1 100644 --- a/book/asciidoc/yesod-for-haskellers.asciidoc +++ b/book/asciidoc/yesod-for-haskellers.asciidoc @@ -158,8 +158,6 @@ import Blaze.ByteString.Builder (Builder, fromByteString) import Blaze.ByteString.Builder.Char.Utf8 (fromShow) import Control.Concurrent (threadDelay) import Control.Monad (forM_) -import Control.Monad.Trans.Class (lift) -import Data.Monoid ((<>)) import Network.HTTP.Types (status200) import Network.Wai (Application, responseStream) @@ -203,9 +201,6 @@ into play: {-# LANGUAGE OverloadedStrings #-} import Blaze.ByteString.Builder (fromByteString) import qualified Data.ByteString as S -import Data.Conduit (Flush (Chunk), ($=)) -import Data.Conduit.Binary (sourceHandle) -import qualified Data.Conduit.List as CL import Network.HTTP.Types (status200) import Network.Wai (Application, responseStream) import Network.Wai.Handler.Warp (run) @@ -749,7 +744,6 @@ import Blaze.ByteString.Builder (fromByteString) import Blaze.ByteString.Builder.Char.Utf8 (fromShow) import Control.Concurrent (threadDelay) import Control.Monad (forM_) -import Data.Monoid ((<>)) import Network.Wai (pathInfo) import Yesod.Core (HandlerT, RenderRoute (..), TypedContent, Yesod, @@ -1077,7 +1071,7 @@ here. Please see the Shakespeare chapter in the book for more information. [source, haskell] ---- {-# LANGUAGE QuasiQuotes #-} -import Data.Text (Text, pack) +import Data.Text (pack) import Text.Julius (Javascript) import Text.Lucius (Css) import Yesod.Core diff --git a/book/asciidoc/yesod-typeclass.asciidoc b/book/asciidoc/yesod-typeclass.asciidoc index 9b5dec2..d30944c 100644 --- a/book/asciidoc/yesod-typeclass.asciidoc +++ b/book/asciidoc/yesod-typeclass.asciidoc @@ -151,7 +151,6 @@ let's see how we could modify Yesod to always produce trailing slashes on URLs: {-# LANGUAGE TypeFamilies #-} import Blaze.ByteString.Builder.Char.Utf8 (fromText) import Control.Arrow ((***)) -import Data.Monoid (mappend) import qualified Data.Text as T import qualified Data.Text.Encoding as TE import Network.HTTP.Types (encodePath) diff --git a/book/asciidoc/yesods-monads.asciidoc b/book/asciidoc/yesods-monads.asciidoc index 6290cf1..6055d07 100644 --- a/book/asciidoc/yesods-monads.asciidoc +++ b/book/asciidoc/yesods-monads.asciidoc @@ -405,7 +405,7 @@ How do we get such an instance? One approach is to simply use the +CRandT+ monad {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Yesod -import Crypto.Random (SystemRandom, newGenIO) +import Crypto.Random (SystemRandom) import Control.Monad.CryptoRandom import Data.ByteString.Base16 (encode) import Data.Text.Encoding (decodeUtf8) @@ -466,7 +466,7 @@ import Control.Monad (join) import Control.Monad.Catch (throwM) import Control.Monad.CryptoRandom import Control.Monad.Error.Class (MonadError (..)) -import Crypto.Random (SystemRandom, newGenIO) +import Crypto.Random (SystemRandom) import Data.ByteString.Base16 (encode) import Data.IORef import Data.Text.Encoding (decodeUtf8) From 653fdec49aaa2667b5545bb5a7976d7a3ca0f1b2 Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Sat, 14 Feb 2026 22:08:50 +0100 Subject: [PATCH 06/29] Fix quoted field attributes --- book/asciidoc/persistent.asciidoc | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/book/asciidoc/persistent.asciidoc b/book/asciidoc/persistent.asciidoc index beed58f..bb4d4a6 100644 --- a/book/asciidoc/persistent.asciidoc +++ b/book/asciidoc/persistent.asciidoc @@ -1088,9 +1088,8 @@ itself; you still need to fill in all values. This will only affect the database schema and automatic migrations. We need to surround the string with single quotes so that the database can -properly interpret it. Finally, Persistent can use double quotes for containing -white space, so if we want to set someone's default home country to be El -Salvador: +properly interpret it. This also allows us to use values containing white space, +so if we want to set someone's default home country to be El Salvador: [source, haskell] @@ -1119,7 +1118,7 @@ Person age Int Maybe created UTCTime default=CURRENT_TIME language String default='Haskell' - country String "default='El Salvador'" + country String default='El Salvador' deriving Show |] @@ -1139,7 +1138,7 @@ share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Person sql=the-person-table id=numeric_id firstName String sql=first_name lastName String sql=fldLastName - age Int "sql=The Age of the Person" + age Int sql="The Age of the Person" PersonName firstName lastName deriving Show |] From b34da933e371acc6423113aecf4f83a8d0c751b6 Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Sun, 15 Feb 2026 17:51:45 +0100 Subject: [PATCH 07/29] Simplify imports for Conduit --- book/asciidoc/case-study-sphinx.asciidoc | 13 ++++++------- book/asciidoc/json-web-service.asciidoc | 2 +- book/asciidoc/persistent.asciidoc | 6 ++---- book/asciidoc/sql-joins.asciidoc | 9 ++++----- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/book/asciidoc/case-study-sphinx.asciidoc b/book/asciidoc/case-study-sphinx.asciidoc index eedfa92..ad5b402 100644 --- a/book/asciidoc/case-study-sphinx.asciidoc +++ b/book/asciidoc/case-study-sphinx.asciidoc @@ -472,7 +472,7 @@ We start the document element with an +id+ attribute, start the content, insert the content, and then close both elements. We use +toPathPiece+ to convert a +DocId+ into a +Text+ value. Next, we need to be able to convert a stream of these entities into a stream of events. For this, we can use the built-in -+concatMap+ function from +Data.Conduit.List+: +CL.concatMap entityToEvents+. ++concatMapC+ function from +Conduit+: +concatMapC entityToEvents+. But what we _really_ want is to stream those events directly from the database. For most of this book, we've used the +selectList+ function, but Persistent @@ -483,7 +483,7 @@ the function: [source, haskell] ---- docSource :: ConduitT () X.Event (YesodDB Searcher) () -docSource = selectSource [] [] .| CL.concatMap entityToEvents +docSource = selectSource [] [] .| concatMapC entityToEvents ---- The .| operator combines two Conduits together into a new Conduit. Now @@ -509,7 +509,7 @@ getXmlpipeR = respondSourceDB "text/xml" $ fullDocSource .| renderBuilder def - .| CL.map Chunk + .| mapC Chunk ---- === Full code @@ -525,10 +525,9 @@ getXmlpipeR = {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-} +import Conduit import Control.Monad (forM) import Control.Monad.Logger (runStdoutLoggingT) -import Data.Conduit -import qualified Data.Conduit.List as CL import Data.Maybe (catMaybes) import Data.Text (Text) import qualified Data.Text as T @@ -717,7 +716,7 @@ getXmlpipeR = respondSourceDB "text/xml" $ fullDocSource .| renderBuilder def - .| CL.map Chunk + .| mapC Chunk entityToEvents :: (Entity Doc) -> [X.Event] entityToEvents (Entity docid doc) = @@ -735,7 +734,7 @@ fullDocSource = do mapM_ yield endEvents docSource :: ConduitT () X.Event (YesodDB Searcher) () -docSource = selectSource [] [] .| CL.concatMap entityToEvents +docSource = selectSource [] [] .| concatMapC entityToEvents toName :: Text -> X.Name toName x = X.Name x (Just "http://sphinxsearch.com/") (Just "sphinx") diff --git a/book/asciidoc/json-web-service.asciidoc b/book/asciidoc/json-web-service.asciidoc index 9759fa1..c119157 100644 --- a/book/asciidoc/json-web-service.asciidoc +++ b/book/asciidoc/json-web-service.asciidoc @@ -16,12 +16,12 @@ This plays out as: [source, haskell] ---- {-# LANGUAGE OverloadedStrings #-} +import Conduit (runConduit, (.|)) import Control.Exception (SomeException) import Control.Exception.Lifted (handle) import Control.Monad.IO.Class (liftIO) import Data.Aeson (Value, encode, object, (.=)) import Data.Aeson.Parser (json) -import Data.Conduit (runConduit, (.|)) import Data.Conduit.Attoparsec (sinkParser) import Network.HTTP.Types (status200, status400) import Network.Wai (Application, Response, responseLBS) diff --git a/book/asciidoc/persistent.asciidoc b/book/asciidoc/persistent.asciidoc index bb4d4a6..69e81ac 100644 --- a/book/asciidoc/persistent.asciidoc +++ b/book/asciidoc/persistent.asciidoc @@ -1460,12 +1460,10 @@ operators. But this is still a good example, so let's roll with it. {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +import Conduit import Database.Persist.TH import Data.Text (Text) import Database.Persist.Sqlite -import Control.Monad.IO.Class (liftIO) -import Data.Conduit -import qualified Data.Conduit.List as CL share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Person @@ -1485,7 +1483,7 @@ main = runSqlite ":memory:" $ do -- Persistent does not provide the LIKE keyword, but we'd like to get the -- whole Snoyman family... let sql = "SELECT name FROM Person WHERE name LIKE '%Snoyman'" - runConduit $ rawQuery sql [] .| CL.mapM_ (liftIO . print) + runConduit $ rawQuery sql [] .| mapM_C (liftIO . print) ---- There is also higher-level support that allows for automated data marshaling. diff --git a/book/asciidoc/sql-joins.asciidoc b/book/asciidoc/sql-joins.asciidoc index 04549f0..e5e3fde 100644 --- a/book/asciidoc/sql-joins.asciidoc +++ b/book/asciidoc/sql-joins.asciidoc @@ -325,7 +325,7 @@ getHomeR = do render <- getUrlRenderParams respondSourceDB typeHtml $ do sendChunkText "Blog posts
        " - blogsSrc .| CL.map (\(E.Value blogid, E.Value title, E.Value name) -> + blogsSrc .| mapC (\(E.Value blogid, E.Value title, E.Value name) -> toFlushBuilder $ [hamlet|
      • @@ -336,7 +336,7 @@ getHomeR = do ---- Notice the usage of +sendChunkText+, which sends some raw +Text+ values over -the network. We then take each of our blog tuples and use conduit's +map+ +the network. We then take each of our blog tuples and use conduit's +mapC+ function to create a streaming value. We use +hamlet+ to get templating, and then pass in our +render+ function to convert the type-safe URLs into their textual versions. Finally, +toFlushBuilder+ converts our +Html+ value into a @@ -374,13 +374,12 @@ For completeness, here's the entire body of the final, streaming example: {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-} +import Conduit import Data.Text (Text) import qualified Database.Esqueleto as E import Database.Esqueleto ((^.)) import Database.Persist.Sqlite import Yesod -import qualified Data.Conduit.List as CL -import Data.Conduit ((.|)) share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Author @@ -423,7 +422,7 @@ getHomeR = do render <- getUrlRenderParams respondSourceDB typeHtml $ do sendChunkText "Blog posts
          " - blogsSrc .| CL.map (\(E.Value blogid, E.Value title, E.Value name) -> + blogsSrc .| mapC (\(E.Value blogid, E.Value title, E.Value name) -> toFlushBuilder $ [hamlet|
        • From b46431ce6721add4fff02ef7d2abb8352db5a6da Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Sun, 15 Feb 2026 20:05:31 +0100 Subject: [PATCH 08/29] Remove obsolete Upstart example --- book/asciidoc/deploying-your-webapp.asciidoc | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/book/asciidoc/deploying-your-webapp.asciidoc b/book/asciidoc/deploying-your-webapp.asciidoc index ba8d1fb..c4dae00 100644 --- a/book/asciidoc/deploying-your-webapp.asciidoc +++ b/book/asciidoc/deploying-your-webapp.asciidoc @@ -233,23 +233,13 @@ responsible to run your own process. I strongly recommend a monitoring utility which will automatically restart your application in case it crashes. There are many great options out there, such as angel or daemontools. -To give a concrete example, here is an Upstart config file. The file must be -placed in +/etc/init/mysite.conf+: - ----- -description "My awesome Yesod application" -start on runlevel [2345]; -stop on runlevel [!2345]; -respawn -chdir /home/michael/sites/mysite -exec /home/michael/sites/mysite/dist/build/mysite/mysite ----- - -Once this is in place, bringing up your application is as simple as -+sudo start mysite+. A similar systemd configuration file placed in +To give a concrete example, here is a systemd configuration file placed in +/etc/systemd/system/yesod-sample.service+: ---- +[Unit] +Description=My awesome Yesod application + [Service] ExecStart=/home/sibi/.local/bin/my-yesod-executable Restart=always From 50307b7fc0cf47fa8dc29037bf582b740d3680ec Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Sun, 15 Feb 2026 21:14:02 +0100 Subject: [PATCH 09/29] Fix typos in "RESTful Content" chapter --- book/asciidoc/restful-content.asciidoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/book/asciidoc/restful-content.asciidoc b/book/asciidoc/restful-content.asciidoc index b425bf9..62e3d1f 100644 --- a/book/asciidoc/restful-content.asciidoc +++ b/book/asciidoc/restful-content.asciidoc @@ -38,13 +38,13 @@ bank account to another. +PUT+:: Create a new resource on the server, or replace an existing one. This method _is_ safe to be called multiple times. -+PATCH+:: Updates the resource partially on the server. When you want -to update one or more field of the resource, this method should be preferred. ++PATCH+:: Updates the resource partially on the server. When you want to update +one or more fields of the resource, this method should be preferred. +DELETE+:: Just like it sounds: wipe out a resource on the server. Calling multiple times should be OK. -To a certain extent, this fits in very well with Haskell philosophy: a +GET+ +To a certain extent, this fits in very well with Haskell's philosophy: a +GET+ request is similar to a pure function, which cannot have side effects. In practice, your +GET+ functions will probably perform +IO+, such as reading information from a database, logging user actions, and so on. From 8ab17a55463f00b734fb374b6f4c1b12e6650c1b Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Sun, 15 Feb 2026 21:15:17 +0100 Subject: [PATCH 10/29] Fix spelling of "MIME type" --- book/asciidoc/deploying-your-webapp.asciidoc | 2 +- book/asciidoc/restful-content.asciidoc | 18 +++++++++--------- book/asciidoc/yesod-for-haskellers.asciidoc | 4 ++-- book/asciidoc/yesod-typeclass.asciidoc | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/book/asciidoc/deploying-your-webapp.asciidoc b/book/asciidoc/deploying-your-webapp.asciidoc index c4dae00..fb11ea3 100644 --- a/book/asciidoc/deploying-your-webapp.asciidoc +++ b/book/asciidoc/deploying-your-webapp.asciidoc @@ -371,7 +371,7 @@ and the last line does the actual rewriting. === FastCGI on lighttpd For this example, I've left off some of the basic FastCGI settings like -mime-types. I also have a more complex file in production that prepends "www." +MIME types. I also have a more complex file in production that prepends "www." when absent and serves static files from a separate domain. However, this should serve to show the basics. diff --git a/book/asciidoc/restful-content.asciidoc b/book/asciidoc/restful-content.asciidoc index 62e3d1f..6842aac 100644 --- a/book/asciidoc/restful-content.asciidoc +++ b/book/asciidoc/restful-content.asciidoc @@ -144,12 +144,12 @@ main = warp 3000 App The +selectRep+ function says ``I'm about to give you some possible representations''. Each +provideRep+ call provides an alternate representation. -Yesod uses the Haskell types to determine the mime type for each +Yesod uses the Haskell types to determine the MIME type for each representation. Since +shamlet+ (a.k.a. simple Hamlet) produces an +Html+ -value, Yesod can determine that the relevant mime type is +text/html+. -Similarly, +object+ generates a JSON value, which implies the mime type +value, Yesod can determine that the relevant MIME type is +text/html+. +Similarly, +object+ generates a JSON value, which implies the MIME type +application/json+. +TypedContent+ is a data type provided by Yesod for some -raw content with an attached mime type. We'll cover it in more detail in a +raw content with an attached MIME type. We'll cover it in more detail in a little bit. To test this out, start up the server and then try running the following @@ -307,7 +307,7 @@ main = warp 3000 App ==== New datatypes Let's say I've come up with some new data format based on using Haskell's -+Show+ instance; I'll call it ``Haskell Show'', and give it a mime type of ++Show+ instance; I'll call it ``Haskell Show'', and give it a MIME type of +text/haskell-show+. And let's say that I decide to include this representation from my web app. How do I do it? For a first attempt, let's use the +TypedContent+ datatype directly. @@ -357,7 +357,7 @@ There are a few important things to note here. aeson's +Value+. * We're using the +TypedContent+ constructor directly. It takes two arguments: - a mime type, and the raw content. Note that +ContentType+ is simply a type + a MIME type, and the raw content. Note that +ContentType+ is simply a type alias for a strict +ByteString+. That's all well and good, but it bothers me that the type signature for @@ -418,7 +418,7 @@ other in this way. +ToTypedContent+ is used internally by Yesod, and is called on the result of all handler functions. As you can see, the implementation is fairly trivial, -simply stating the mime type and then calling out to +toContent+. +simply stating the MIME type and then calling out to +toContent+. Finally, let's make this a bit more complicated, and get this to play well with +selectRep+. @@ -479,14 +479,14 @@ main = warp 3000 App The important addition here is the +HasContentType+ instance. This may seem redundant, but it serves an important role. We need to be able to determine the -mime type of a possible representation _before creating that representation_. +MIME type of a possible representation _before creating that representation_. +ToTypedContent+ only works on a concrete value, and therefore can't be used before creating the value. +getContentType+ instead takes a proxy value, indicating the type without providing anything concrete. NOTE: If you want to provide a representation for a value that doesn't have a +HasContentType+ instance, you can use the +provideRepType+ function, which -requires you to explicitly state the mime type present. +requires you to explicitly state the MIME type present. === Other request headers diff --git a/book/asciidoc/yesod-for-haskellers.asciidoc b/book/asciidoc/yesod-for-haskellers.asciidoc index 6fa3fe1..313528a 100644 --- a/book/asciidoc/yesod-for-haskellers.asciidoc +++ b/book/asciidoc/yesod-for-haskellers.asciidoc @@ -497,9 +497,9 @@ typeclass, and what does it have to do with our +Html+ response? Let's start by answering my question from above: no, Yesod does *not* in any way hard code support for +Html+. A handler function can return any value that has an instance of +ToTypedContent+. This typeclass (which we will examine in a moment) -provides both a mime-type and a representation of the data that WAI can +provides both a MIME type and a representation of the data that WAI can consume. +yesodRunner+ then converts that into a WAI response, setting the -+Content-Type+ response header to the mime type, using a 200 OK status code, ++Content-Type+ response header to the MIME type, using a 200 OK status code, and sending the response body. ==== (To)Content, (To)TypedContent diff --git a/book/asciidoc/yesod-typeclass.asciidoc b/book/asciidoc/yesod-typeclass.asciidoc index d30944c..852957b 100644 --- a/book/asciidoc/yesod-typeclass.asciidoc +++ b/book/asciidoc/yesod-typeclass.asciidoc @@ -383,7 +383,7 @@ really want is to store this content in an external file and then refer to it from the HTML. This is where +addStaticContent+ comes in. It takes three arguments: the -filename extension of the content (+css+ or +js+), the mime-type of the content +filename extension of the content (+css+ or +js+), the MIME type of the content (+text/css+ or +text/javascript+) and the content itself. It will then return one of three possible results: From 6e38d3eb58da32a96217a45409ca885f76d94241 Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Sun, 15 Feb 2026 20:49:56 +0100 Subject: [PATCH 11/29] Fix link to `aeson` documentation --- book/asciidoc/restful-content.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/asciidoc/restful-content.asciidoc b/book/asciidoc/restful-content.asciidoc index 6842aac..3a76cd6 100644 --- a/book/asciidoc/restful-content.asciidoc +++ b/book/asciidoc/restful-content.asciidoc @@ -210,7 +210,7 @@ main = L.putStrLn $ encode $ Person "Michael" 28 ---- I won't go into further detail on +aeson+, as -link:https://www.fpcomplete.com/haddocks/aeson[the Haddock documentation] +link:https://hackage.haskell.org/package/aeson[the Haddock documentation] already provides a great introduction to the library. What I've described so far is enough to understand our convenience functions. From cb9297549603731057da340155b89075d975a461 Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Thu, 19 Feb 2026 16:52:46 +0100 Subject: [PATCH 12/29] Add missing language extensions --- .../authentication-and-authorization.asciidoc | 6 ++++++ book/asciidoc/blog-example-advanced.asciidoc | 20 +++++++++++++++---- book/asciidoc/case-study-sphinx.asciidoc | 6 ++++++ book/asciidoc/persistent.asciidoc | 15 ++++++++++++++ book/asciidoc/sql-joins.asciidoc | 12 +++++++++++ book/asciidoc/yesods-monads.asciidoc | 6 ++++++ 6 files changed, 61 insertions(+), 4 deletions(-) diff --git a/book/asciidoc/authentication-and-authorization.asciidoc b/book/asciidoc/authentication-and-authorization.asciidoc index 739466a..a5df1b8 100644 --- a/book/asciidoc/authentication-and-authorization.asciidoc +++ b/book/asciidoc/authentication-and-authorization.asciidoc @@ -238,15 +238,21 @@ verification link is printed in the console. [source, haskell] ---- +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} import Control.Monad (join) import Control.Monad.Logger (runNoLoggingT) import Data.Maybe (isJust) diff --git a/book/asciidoc/blog-example-advanced.asciidoc b/book/asciidoc/blog-example-advanced.asciidoc index 336e09c..020df02 100644 --- a/book/asciidoc/blog-example-advanced.asciidoc +++ b/book/asciidoc/blog-example-advanced.asciidoc @@ -15,10 +15,22 @@ your individual Haskell files. [source, haskell] ---- -{-# LANGUAGE OverloadedStrings, TypeFamilies, QuasiQuotes, - TemplateHaskell, GADTs, FlexibleContexts, - MultiParamTypeClasses, DeriveDataTypeable, - GeneralizedNewtypeDeriving, ViewPatterns #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} +{-# LANGUAGE ViewPatterns #-} ---- Now our imports. diff --git a/book/asciidoc/case-study-sphinx.asciidoc b/book/asciidoc/case-study-sphinx.asciidoc index ad5b402..51c5b32 100644 --- a/book/asciidoc/case-study-sphinx.asciidoc +++ b/book/asciidoc/case-study-sphinx.asciidoc @@ -516,14 +516,20 @@ getXmlpipeR = [source, haskell] ---- +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE ViewPatterns #-} import Conduit import Control.Monad (forM) diff --git a/book/asciidoc/persistent.asciidoc b/book/asciidoc/persistent.asciidoc index 69e81ac..79229ef 100644 --- a/book/asciidoc/persistent.asciidoc +++ b/book/asciidoc/persistent.asciidoc @@ -66,6 +66,7 @@ The required dependencies for the below are: persistent and persistent-sqlite. {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} import Control.Monad.IO.Class (liftIO) import Database.Persist import Database.Persist.Sqlite @@ -226,6 +227,7 @@ entities once. Let's see a quick example: {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} import Database.Persist.TH import Database.Persist.Sqlite @@ -345,6 +347,7 @@ pass it off to other Persistent functions. {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} import Database.Persist import Database.Persist.Sqlite @@ -462,6 +465,7 @@ to _ask_ it to help. Let's see what this looks like: {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} import Database.Persist import Database.Persist.Sqlite @@ -513,6 +517,7 @@ Persistent provides a helper function, +mkMigrate+: {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} import Database.Persist.Sqlite import Database.Persist.TH @@ -612,6 +617,7 @@ in Haskell as a data constructor. {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH @@ -1015,6 +1021,7 @@ database, we want to just use the current date-time for that timestamp. {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH @@ -1064,6 +1071,7 @@ favorite programming language: {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} import Database.Persist.Sqlite import Database.Persist.TH import Data.Time @@ -1108,6 +1116,7 @@ so if we want to set someone's default home country to be El Salvador: {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} import Database.Persist.Sqlite import Database.Persist.TH import Data.Time @@ -1170,6 +1179,7 @@ the related entity. So if a person has many cars: {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH @@ -1218,6 +1228,7 @@ want to track which people have shopped in which stores: {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH @@ -1398,6 +1409,7 @@ derivePersistField "Employment" {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} import Database.Persist.Sqlite import Database.Persist.TH import Employment @@ -1460,6 +1472,7 @@ operators. But this is still a good example, so let's roll with it. {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} import Conduit import Database.Persist.TH import Data.Text (Text) @@ -1516,6 +1529,7 @@ the database via the +runDB+ method. Let's see this in action. {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} {-# LANGUAGE ViewPatterns #-} import Yesod import Database.Persist.Sqlite @@ -1643,6 +1657,7 @@ To keep the examples in this chapter simple, we've used the SQLite backend. Just {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} import Control.Monad.IO.Class (liftIO) import Control.Monad.Logger (runStderrLoggingT) import Database.Persist diff --git a/book/asciidoc/sql-joins.asciidoc b/book/asciidoc/sql-joins.asciidoc index e5e3fde..dd645d3 100644 --- a/book/asciidoc/sql-joins.asciidoc +++ b/book/asciidoc/sql-joins.asciidoc @@ -30,15 +30,21 @@ the blog title and the author: [source, haskell] ---- +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE ViewPatterns #-} import Data.Text (Text) import Database.Persist.Sqlite @@ -364,15 +370,21 @@ For completeness, here's the entire body of the final, streaming example: [source, haskell] ---- +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE ViewPatterns #-} import Conduit import Data.Text (Text) diff --git a/book/asciidoc/yesods-monads.asciidoc b/book/asciidoc/yesods-monads.asciidoc index 6055d07..f640e35 100644 --- a/book/asciidoc/yesods-monads.asciidoc +++ b/book/asciidoc/yesods-monads.asciidoc @@ -128,14 +128,20 @@ itself. This is a boon for modularity, as this +Widget+ can be used in any [source, haskell] ---- +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} import Control.Monad.Logger (runNoLoggingT) import Data.Text (Text) import Data.Time From bf8ac561e95092fd536689659d5e7b0c938ea4bc Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Thu, 19 Feb 2026 18:03:38 +0100 Subject: [PATCH 13/29] Fix typo in "Sphinx-based Search" example --- book/asciidoc/case-study-sphinx.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/asciidoc/case-study-sphinx.asciidoc b/book/asciidoc/case-study-sphinx.asciidoc index 51c5b32..f107fc1 100644 --- a/book/asciidoc/case-study-sphinx.asciidoc +++ b/book/asciidoc/case-study-sphinx.asciidoc @@ -372,7 +372,7 @@ several hundred kilobytes. If we take a non-streaming approach, this can lead to huge memory usage and slow response times. So how exactly do we create a streaming response? Yesod provides a helper -function for this case: +responseSourceDB+. This function takes two arguments: +function for this case: +respondSourceDB+. This function takes two arguments: a content type, and a Conduit providing a stream of blaze-builder ++Builder++s. Yesod then handles all of the issues of grabbing a database connection from the connection pool, starting a transaction, and streaming the From d59d246076e1e408c84ec29c93d5339ac8c5262f Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Thu, 19 Feb 2026 19:16:30 +0100 Subject: [PATCH 14/29] Remove obsolete example in "Scaffolding" chapter --- book/asciidoc/scaffolding-and-the-site-template.asciidoc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/book/asciidoc/scaffolding-and-the-site-template.asciidoc b/book/asciidoc/scaffolding-and-the-site-template.asciidoc index d8389c4..19c2b7a 100644 --- a/book/asciidoc/scaffolding-and-the-site-template.asciidoc +++ b/book/asciidoc/scaffolding-and-the-site-template.asciidoc @@ -165,8 +165,7 @@ implementations of +Yesod+ typeclass methods. The +Import+ module was born out of a few commonly recurring patterns. -* I want to define some helper functions (maybe the +<> = mappend+ - operator) to be used by all handlers. +* I want to define some helper functions to be used by all handlers. * I'm always adding the same five import statements (+Data.Text+, +Control.Applicative+, etc) to every handler module. From b00ff97df01cf4d1d1d9784b886ff22f80a1dbf5 Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Thu, 19 Feb 2026 19:21:13 +0100 Subject: [PATCH 15/29] Replace `mappend` with `(<>)` --- book/asciidoc/yesod-typeclass.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/asciidoc/yesod-typeclass.asciidoc b/book/asciidoc/yesod-typeclass.asciidoc index 852957b..f5e8a26 100644 --- a/book/asciidoc/yesod-typeclass.asciidoc +++ b/book/asciidoc/yesod-typeclass.asciidoc @@ -165,7 +165,7 @@ mkYesod "Slash" [parseRoutes| instance Yesod Slash where joinPath _ ar pieces' qs' = - fromText ar `mappend` encodePath pieces qs + fromText ar <> encodePath pieces qs where qs = map (TE.encodeUtf8 *** go) qs' go "" = Nothing From 3671ba8cbe7454d129799f0d4f0c58f77a21384e Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Thu, 19 Feb 2026 19:35:27 +0100 Subject: [PATCH 16/29] Fix typo in the "Understanding a Request" chapter --- book/asciidoc/understanding-request.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/asciidoc/understanding-request.asciidoc b/book/asciidoc/understanding-request.asciidoc index 2374493..d11eacf 100644 --- a/book/asciidoc/understanding-request.asciidoc +++ b/book/asciidoc/understanding-request.asciidoc @@ -378,7 +378,7 @@ Otherwise, we want to return a 405 bad method response, and therefore use the +badMethod+ handler. At this point, we’ve come full circle to our original handler discussion. You can see that we’re using +yesodRunner+ to execute our handler function. As a reminder, this will take our environment and WAI -+Request+, convert it to a +YesodRequest+, constructor a +RunHandlerEnv+, hand that ++Request+, convert it to a +YesodRequest+, construct a +RunHandlerEnv+, hand that to the handler function, and then convert the resulting +YesodResponse+ into a WAI +Response+. From 69e37479122c1cac59fd9abf6549fffb43fee33c Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Thu, 19 Feb 2026 22:58:51 +0100 Subject: [PATCH 17/29] Match qualifier names with `yesod-core` --- book/asciidoc/understanding-request.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/book/asciidoc/understanding-request.asciidoc b/book/asciidoc/understanding-request.asciidoc index d11eacf..d560269 100644 --- a/book/asciidoc/understanding-request.asciidoc +++ b/book/asciidoc/understanding-request.asciidoc @@ -98,8 +98,8 @@ but how does Yesod know how to deal with +Html+? The answer lies in the [source, haskell] ---- -data Content = ContentBuilder !BBuilder.Builder !(Maybe Int) -- ^ The content and optional content length. - | ContentSource !(ConduitT () (Flush BBuilder.Builder) (ResourceT IO) ()) +data Content = ContentBuilder !BB.Builder !(Maybe Int) -- ^ The content and optional content length. + | ContentSource !(ConduitT () (Flush BB.Builder) (ResourceT IO) ()) | ContentFile !FilePath !(Maybe FilePart) | ContentDontEvaluate !Content data TypedContent = TypedContent !ContentType !Content From aa1a9ff2ef6e48059c05b0e61969e424998ed37e Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Fri, 20 Feb 2026 15:39:29 +0100 Subject: [PATCH 18/29] Reflect updated `PageContent` type in examples --- book/asciidoc/widgets.asciidoc | 7 ++++--- book/asciidoc/yesod-typeclass.asciidoc | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/book/asciidoc/widgets.asciidoc b/book/asciidoc/widgets.asciidoc index 093b5d8..a8631d0 100644 --- a/book/asciidoc/widgets.asciidoc +++ b/book/asciidoc/widgets.asciidoc @@ -387,9 +387,10 @@ The answer is +widgetToPageContent+. Let's look at some (simplified) types: [source, haskell] ---- data PageContent url = PageContent - { pageTitle :: Html - , pageHead :: HtmlUrl url - , pageBody :: HtmlUrl url + { pageTitle :: !Html + , pageDescription :: !(Maybe Text) + , pageHead :: !(HtmlUrl url) + , pageBody :: !(HtmlUrl url) } widgetToPageContent :: Widget -> Handler (PageContent url) ---- diff --git a/book/asciidoc/yesod-typeclass.asciidoc b/book/asciidoc/yesod-typeclass.asciidoc index f5e8a26..5bfdfa6 100644 --- a/book/asciidoc/yesod-typeclass.asciidoc +++ b/book/asciidoc/yesod-typeclass.asciidoc @@ -272,7 +272,7 @@ mkYesod "App" [parseRoutes| instance Yesod App where defaultLayout contents = do - PageContent title headTags bodyTags <- widgetToPageContent contents + PageContent title _desc headTags bodyTags <- widgetToPageContent contents mmsg <- getMessage withUrlRenderer [hamlet| $doctype 5 From a9a347d1aa87d7c7de44b4302f4987c18ed610ec Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Sun, 22 Feb 2026 12:35:47 +0100 Subject: [PATCH 19/29] Update `PersistValue` to current implementation --- book/asciidoc/persistent.asciidoc | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/book/asciidoc/persistent.asciidoc b/book/asciidoc/persistent.asciidoc index 79229ef..438e60e 100644 --- a/book/asciidoc/persistent.asciidoc +++ b/book/asciidoc/persistent.asciidoc @@ -177,11 +177,17 @@ data PersistValue | PersistNull | PersistList [PersistValue] | PersistMap [(Text, PersistValue)] - | PersistObjectId ByteString - -- ^ Intended especially for MongoDB backend - | PersistDbSpecific ByteString - -- ^ Using 'PersistDbSpecific' allows you to use types - -- specific to a particular backend + | -- | Intended especially for MongoDB backend + PersistObjectId ByteString + | -- | Intended especially for PostgreSQL backend for text arrays + PersistArray [PersistValue] + | -- | This constructor is used to specify some raw literal value for the + -- backend. The 'LiteralType' value specifies how the value should be + -- escaped. This can be used to make special, custom types avaialable + -- in the back end. + -- + -- @since 2.12.0.0 + PersistLiteral_ LiteralType ByteString ---- A +PersistValue+ correlates to a column in a SQL database. In our person example From b7d5270bc463bbf54385e12de180aa0d3b359523 Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Sun, 22 Feb 2026 15:51:03 +0100 Subject: [PATCH 20/29] Update generated code examples --- book/asciidoc/persistent.asciidoc | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/book/asciidoc/persistent.asciidoc b/book/asciidoc/persistent.asciidoc index 438e60e..62452b2 100644 --- a/book/asciidoc/persistent.asciidoc +++ b/book/asciidoc/persistent.asciidoc @@ -276,22 +276,22 @@ data Person = Person type PersonId = Key Person instance PersistEntity Person where - newtype Key Person = PersonKey (BackendKey SqlBackend) + newtype Key Person = PersonKey {unPersonKey :: (BackendKey SqlBackend)} deriving (PersistField, Show, Eq, Read, Ord) -- A Generalized Algebraic Datatype (GADT). -- This gives us a type-safe approach to matching fields with -- their datatypes. - data EntityField Person typ where - PersonId :: EntityField Person PersonId - PersonName :: EntityField Person String - PersonAge :: EntityField Person Int + data EntityField Person typ + = (typ ~ PersonId) => PersonId + | (typ ~ String) => PersonName + | (typ ~ Int) => PersonAge data Unique Person type PersistEntityBackend Person = SqlBackend toPersistFields (Person name age) = - [ SomePersistField name - , SomePersistField age + [ toPersistValue name + , toPersistValue age ] fromPersistValues [nameValue, ageValue] = Person @@ -301,29 +301,32 @@ instance PersistEntity Person where -- Information on each field, used internally to generate SQL statements persistFieldDef PersonId = FieldDef - (HaskellName "Id") - (DBName "id") + (FieldNameHS "Id") + (FieldNameDB "id") (FTTypeCon Nothing "PersonId") SqlInt64 [] True NoReference + ... persistFieldDef PersonName = FieldDef - (HaskellName "name") - (DBName "name") + (FieldNameHS "name") + (FieldNameDB "name") (FTTypeCon Nothing "String") SqlString [] True NoReference + ... persistFieldDef PersonAge = FieldDef - (HaskellName "age") - (DBName "age") + (FieldNameHS "age") + (FieldNameDB "age") (FTTypeCon Nothing "Int") SqlInt64 [] True NoReference + ... ---- As you might expect, our +Person+ datatype closely matches the definition we From a57e33f8b2240f41beab177829883c2bb20e9cb4 Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Mon, 23 Feb 2026 12:43:44 +0100 Subject: [PATCH 21/29] Reflect the split of `PersistStore` --- book/asciidoc/yesods-monads.asciidoc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/book/asciidoc/yesods-monads.asciidoc b/book/asciidoc/yesods-monads.asciidoc index f640e35..0188ec4 100644 --- a/book/asciidoc/yesods-monads.asciidoc +++ b/book/asciidoc/yesods-monads.asciidoc @@ -83,13 +83,13 @@ more generic +HandlerT+ and +WidgetT+. Each of those transformers takes two type parameters: your foundation data type, and a base monad. The most commonly used base monad is +IO+. -In persistent, we have a typeclass called +PersistStore+. This typeclass -defines all of the primitive operations you can perform on a database, like -+get+. There are instances of this typeclass for each database backend -supported by persistent. For example, for SQL databases, there is a datatype -called +SqlBackend+. We then use a standard +ReaderT+ transformer to provide -that +SqlBackend+ value to all of our operations. This means that you can run -a SQL database with any underlying monad which is an instance of +MonadIO+. The +In persistent, we have a few typeclasses that define all of the primitive +operations you can perform on a database, like +get+ in +PersistStoreRead+. +There are instances of these typeclasses for each database backend supported by +persistent. For example, for SQL databases, there is a datatype called ++SqlBackend+. We then use a standard +ReaderT+ transformer to provide that ++SqlBackend+ value to all of our operations. This means that you can run a SQL +database with any underlying monad which is an instance of +MonadIO+. The takeaway here is that we can layer our Persistent transformer on top of +Handler+ or +Widget+. From 60a6b459ea42d0e44b40ed83f8aa76b9c8321095 Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Mon, 23 Feb 2026 12:22:47 +0100 Subject: [PATCH 22/29] Reflect the change from `HandlerT` to `HandlerFor` --- book/asciidoc/persistent.asciidoc | 4 +- book/asciidoc/understanding-request.asciidoc | 8 +-- book/asciidoc/yesod-for-haskellers.asciidoc | 51 ++++++++++---------- book/asciidoc/yesods-monads.asciidoc | 39 +++++++++------ 4 files changed, 56 insertions(+), 46 deletions(-) diff --git a/book/asciidoc/persistent.asciidoc b/book/asciidoc/persistent.asciidoc index 62452b2..7861de0 100644 --- a/book/asciidoc/persistent.asciidoc +++ b/book/asciidoc/persistent.asciidoc @@ -1614,11 +1614,11 @@ functions we've spoken about so far, such as +insert+ and +selectList+. [NOTE] ==== -The type of +runDB+ is +YesodDB site a -> HandlerT site IO a+. +YesodDB+ is defined as: +The type of +runDB+ is +YesodDB site a -> HandlerFor site a+. +YesodDB+ is defined as: [source, haskell] ---- -type YesodDB site = ReaderT (YesodPersistBackend site) (HandlerT site IO) +type YesodDB site = ReaderT (YesodPersistBackend site) (HandlerFor site) ---- Since it is built on top of the +YesodPersistBackend+ associated type, it uses diff --git a/book/asciidoc/understanding-request.asciidoc b/book/asciidoc/understanding-request.asciidoc index d560269..8acbf47 100644 --- a/book/asciidoc/understanding-request.asciidoc +++ b/book/asciidoc/understanding-request.asciidoc @@ -75,7 +75,7 @@ NOTE: Yesod uses +ResourceT+ for exception safety, instead of continuation passing style. This makes it much easier to write exception safe code in Yesod. But as a Yesod user, you never really see +YesodApp+. There’s another layer -on top of that which you are used to dealing with: +HandlerT+. When you write +on top of that which you are used to dealing with: +HandlerFor+. When you write handler functions, you need to have access to three different things: * The +YesodRequest+ value for the current request. @@ -86,7 +86,7 @@ handler functions, you need to have access to three different things: So when you’re writing a handler function, you’re essentially just writing in a +ReaderT+ transformer that has access to all of this information. The -+runHandler+ function will turn a +HandlerT+ into a +YesodApp+. +yesodRunner+ takes this ++runHandler+ function will turn a +HandlerFor+ into a +YesodApp+. +yesodRunner+ takes this a step further and converts all the way to a WAI +Application+. ==== Content @@ -313,7 +313,7 @@ components to make our site work. Let’s start with the simplest: the [source, haskell] ---- -type Handler = HandlerT App IO +type Handler = HandlerFor App ---- Next is the type-safe URL and its rendering function. The rendering function is @@ -504,7 +504,7 @@ instance RenderRoute App where let (ps, qs) = renderRoute subRoute in ("my-subsite" : ps, qs) -type Handler = HandlerT App IO +type Handler = HandlerFor App instance Yesod App diff --git a/book/asciidoc/yesod-for-haskellers.asciidoc b/book/asciidoc/yesod-for-haskellers.asciidoc index 313528a..4bed325 100644 --- a/book/asciidoc/yesod-for-haskellers.asciidoc +++ b/book/asciidoc/yesod-for-haskellers.asciidoc @@ -371,12 +371,12 @@ off our application to Warp, just like we did in the past. Congratulations, you've now seen your first Yesod application! (Or, at least your first Yesod application in this chapter.) -=== The HandlerT monad transformer +=== The HandlerFor monad While that example was technically using Yesod, it was incredibly uninspiring. There's no question that Yesod did nothing more than get in our way relative to WAI. And that's because we haven't started taking advantage of any of Yesod's -features. Let's address that, starting with the +HandlerT+ monad transformer. +features. Let's address that, starting with the +HandlerFor+ monad. There are many common things you'd want to do when handling a single request, e.g.: @@ -386,11 +386,10 @@ e.g.: * Return a 404 not found response. * Do some logging. -To encapsulate all of this common functionality, Yesod provides a +HandlerT+ -monad transformer. The vast majority of the code you write in Yesod will live -in this transformer, so you should get acquainted with it. Let's start off by -replacing our previous +YesodDispatch+ instance with a new one that takes -advantage of +HandlerT+: +To encapsulate all of this common functionality, Yesod provides the +HandlerFor+ +monad. The vast majority of the code you write in Yesod will live in this monad, +so you should get acquainted with it. Let's start off by replacing our previous ++YesodDispatch+ instance with a new one that takes advantage of +HandlerFor+: [source, haskell] ---- @@ -399,7 +398,7 @@ advantage of +HandlerT+: import Network.Wai (pathInfo) import Network.Wai.Handler.Warp (run) import qualified Text.Blaze.Html5 as H -import Yesod.Core (HandlerT, Html, RenderRoute (..), +import Yesod.Core (HandlerFor, Html, RenderRoute (..), Yesod, YesodDispatch (..), getYesod, notFound, toWaiApp, yesodRunner) @@ -418,7 +417,7 @@ instance RenderRoute App where , [] -- empty query string ) -getHomeR :: HandlerT App IO Html +getHomeR :: HandlerFor App Html getHomeR = do site <- getYesod return $ welcomeMessage site @@ -447,9 +446,9 @@ main = do +getHomeR+ is our first handler function. (That name is yet another naming convention in the Yesod world: the lower case HTTP request method, followed by -the route constructor name.) Notice its signature: +HandlerT App IO Html+. It's -so common to have the monad stack +HandlerT App IO+ that most applications have -a type synonym for it, +type Handler = HandlerT App IO+. The function is +the route constructor name.) Notice its signature: +HandlerFor App Html+. It's +so common to have the monad +HandlerFor App+ that most applications have +a type synonym for it, +type Handler = HandlerFor App+. The function is returning some +Html+. You might be wondering if Yesod is hard-coded to only work with +Html+ values. We'll explain that detail in a moment. @@ -459,12 +458,12 @@ build up more interesting handlers as we continue. The implementation of +yesodDispatch+ is now quite different. The key to it is the +yesodRunner+ function, which is a low-level function for converting -+HandlerT+ stacks into WAI ++Application++s. Let's look at its type signature: ++HandlerFor+ monads into WAI ++Application++s. Let's look at its type signature: [source, haskell] ---- yesodRunner :: (ToTypedContent res, Yesod site) - => HandlerT site IO res + => HandlerFor site res -> YesodRunnerEnv site -> Maybe (Route site) -> Application @@ -485,7 +484,7 @@ that our parsing and rendering functions remain in proper alignment. We'll discuss how Yesod deals with that later. Coming back to the parameters to +yesodRunner+: we've now addressed the +Maybe -(Route site)+ and +YesodRunerEnv site+. To get our +HandlerT site IO res+ +(Route site)+ and +YesodRunerEnv site+. To get our +HandlerFor site res+ value, we pattern match on +maybeRoute+. If it's +Just HomeR+, we use +getHomeR+. Otherwise, we use the +notFound+ function, which is a built-in function that returns a 404 not found response, using your default site @@ -599,7 +598,7 @@ import Data.Text (Text) import Network.Wai (pathInfo) import Network.Wai.Handler.Warp (run) import qualified Text.Blaze.Html5 as H -import Yesod.Core (HandlerT, Html, RenderRoute (..), +import Yesod.Core (HandlerFor, Html, RenderRoute (..), TypedContent, Value, Yesod, YesodDispatch (..), getYesod, notFound, object, provideRep, @@ -623,7 +622,7 @@ instance RenderRoute App where , [] -- empty query string ) -getHomeR :: HandlerT App IO TypedContent +getHomeR :: HandlerFor App TypedContent getHomeR = do site <- getYesod selectRep $ do @@ -678,11 +677,11 @@ Haskell Center, or deployment tools like Keter. === Writing handlers Since the vast majority of your application will end up living in the -+HandlerT+ monad transformer, it's not surprising that there are quite a few -functions that work in that context. +HandlerT+ is an instance of many common -typeclasses, including +MonadIO+, +MonadTrans+, +MonadBaseControl+, -+MonadLogger+ and +MonadResource+, and so can automatically take advantage of -those functionalities. ++HandlerFor+ monad, it's not surprising that there are quite a few functions +that work in that context. +HandlerFor+ is an instance of many common +typeclasses, including +MonadIO+, +MonadUnliftIO+, +MonadLogger+ and ++MonadResource+, and so can automatically take advantage of those +functionalities. In addition to that standard functionality, the following are some common categories of functions. The only requirement Yesod places on your handler @@ -745,7 +744,7 @@ import Blaze.ByteString.Builder.Char.Utf8 (fromShow) import Control.Concurrent (threadDelay) import Control.Monad (forM_) import Network.Wai (pathInfo) -import Yesod.Core (HandlerT, RenderRoute (..), +import Yesod.Core (HandlerFor, RenderRoute (..), TypedContent, Yesod, YesodDispatch (..), liftIO, notFound, respondSource, @@ -766,7 +765,7 @@ instance RenderRoute App where , [] -- empty query string ) -getHomeR :: HandlerT App IO TypedContent +getHomeR :: HandlerFor App TypedContent getHomeR = respondSource "text/plain" $ do sendChunkBS "Starting streaming response.\n" sendChunkText "Performing some I/O.\n" @@ -797,8 +796,8 @@ main = warp 3000 App === Dynamic parameters -Now that we've finished our detour into the details of the +HandlerT+ -transformer, let's get back to higher-level Yesod request processing. So far, +Now that we've finished our detour into the details of the +HandlerFor+ +monad, let's get back to higher-level Yesod request processing. So far, all of our examples have dealt with a single supported request route. Let's make this more interesting. We now want to have an application which serves Fibonacci numbers. If you make a request to +/fib/5+, it will return the fifth diff --git a/book/asciidoc/yesods-monads.asciidoc b/book/asciidoc/yesods-monads.asciidoc index 0188ec4..d3160cc 100644 --- a/book/asciidoc/yesods-monads.asciidoc +++ b/book/asciidoc/yesods-monads.asciidoc @@ -69,7 +69,7 @@ of transformers you have around an +IO+, there's still an +IO+ at the core, meaning you can perform I/O in any of these monad transformer stacks. You'll often see code that looks like +liftIO $ putStrLn "Hello There!"+. -=== The Three Transformers +=== The Three Monads NOTE: In earlier versions of Yesod, +Handler+ and +Widget+ were far more magical and scary. Since version 1.2, things are much simplified. So if you @@ -77,11 +77,24 @@ remember reading some scary stuff about fake transformers and subsite parameters, rest assured: you haven't gone crazy, things have actually changed a bit. The story with persistent is likewise much simpler. -We've already discussed two of our transformers previously: +Handler+ and -+Widget+. Remember that these are each application-specific synonyms for the -more generic +HandlerT+ and +WidgetT+. Each of those transformers takes two -type parameters: your foundation data type, and a base monad. The most commonly -used base monad is +IO+. +Prior to Yesod 1.6, +Handler+ and +Widget+ were implemented via monad +transformers. Nowadays, we use the +link:https://academy.fpblock.com/blog/2017/06/readert-design-pattern[ReaderT +pattern]. +Handler+ and +Widget+ are application-specific synonyms for the more +generic +HandlerFor+ and +WidgetFor+. + +[source, haskell] +---- +newtype HandlerFor site a = HandlerFor + { unHandlerFor :: HandlerData site site -> IO a + } + +newtype WidgetFor site a = WidgetFor + { unWidgetFor :: WidgetData site -> IO a + } +---- + ++site+ is your foundation data type. In persistent, we have a few typeclasses that define all of the primitive operations you can perform on a database, like +get+ in +PersistStoreRead+. @@ -101,7 +114,7 @@ more convenience, we have a type synonym called +YesodDB+ which is defined as: [source, haskell] ---- -type YesodDB site = ReaderT (YesodPersistBackend site) (HandlerT site IO) +type YesodDB site = ReaderT (YesodPersistBackend site) (HandlerFor site) ---- Our database actions will then have types that look like +YesodDB MyApp @@ -397,7 +410,7 @@ However, this results in an error message along the lines of: [source, errormsg] ---- - No instance for (MonadCRandom e0 (HandlerT App IO)) + No instance for 'MonadCRandom e0 (HandlerFor App)' arising from a use of ‘getBytes’ In a stmt of a 'do' block: randomBS <- getBytes 128 ---- @@ -441,11 +454,9 @@ main :: IO () main = warp 3000 App ---- -Note that what we're doing is layering the +CRandT+ transformer on *top* of the -+HandlerT+ transformer. It does not work to do things the other way around: -Yesod itself would ultimately have to unwrap the +CRandT+ transformer, and it -has no knowledge of how to do so. Notice that this is the same approach we take -with Persistent: its transformer goes on top of +HandlerT+. +Note that what we're doing is layering the +CRandT+ transformer on top of the ++HandlerFor+ monad. This is the same approach we take with Persistent: its +transformer also goes on top of +HandlerFor+. But there are two downsides to this approach: @@ -540,7 +551,7 @@ This really comes down to a few different concepts: . However, we *have* gained some complexity in needing a +MonadCRandom+ instance. Since this is a book on Yesod, and not on +monadcryptorandom+, I'm not going to go into details on this instance, but I encourage you to inspect it, and if you're interested, compare it to the instance for +CRandT+. Hopefully, this helps get across an important point: the power of the -+HandlerT+ transformer. By just providing you with a readable environment, ++ReaderT+ pattern. By just providing you with a readable environment, you're able to recreate a +StateT+ transformer by relying on mutable references. In fact, if you rely on the underlying +IO+ monad for runtime exceptions, you can implement most cases of +ReaderT+, +WriterT+, +StateT+, and From be58ca8ecece27d831517ba7af8767b2b6e42e82 Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Wed, 25 Feb 2026 18:17:38 +0100 Subject: [PATCH 23/29] Remove obsolete section about Yesod monads --- book/asciidoc/yesods-monads.asciidoc | 65 ---------------------------- 1 file changed, 65 deletions(-) diff --git a/book/asciidoc/yesods-monads.asciidoc b/book/asciidoc/yesods-monads.asciidoc index d3160cc..6db4b1c 100644 --- a/book/asciidoc/yesods-monads.asciidoc +++ b/book/asciidoc/yesods-monads.asciidoc @@ -317,71 +317,6 @@ many common monad transformers. But if you want to, you can wrap up the call to +runInputGet+ above using +handlerToWidget+, and everything will work the same. -=== Performance and error messages - -NOTE: You can consider this section extra credit. It gets into some of the -design motivation behind Yesod, which isn't necessary for usage of Yesod. - -At this point, you may be just a bit confused. As I mentioned above, the -+Widget+ synonym uses +IO+ as its base monad, not +Handler+. So how can -+Widget+ perform +Handler+ actions? And why *not* just make +Widget+ a -transformer on top of +Handler+, and then use +lift+ instead of this special -+handlerToWidget+? And finally, I mentioned that +Widget+ and +Handler+ were -both instances of +MonadResource+. If you're familiar with +MonadResource+, you -may be wondering why +ResourceT+ doesn't appear in the monad transformer stack. - -The fact of the matter is, there's a much simpler (in terms of implementation) -approach we could take for all of these monad transformers. +Handler+ could be -a transformer on top of +ResourceT IO+ instead of just +IO+, which would be a -bit more accurate. And +Widget+ could be layered on top of +Handler+. The end -result would look something like this: - -[source, haskell] ----- -type Handler = HandlerT App (ResourceT IO) -type Widget = WidgetT App (HandlerT App (ResourceT IO)) ----- - -Doesn't look too bad, especially since you mostly deal with the more friendly -type synonyms instead of directly with the transformer types. The problem is -that any time those underlying transformers leak out, these larger type -signatures can be incredibly confusing. And the most common time for them to -leak out is in error messages, when you're probably already pretty confused! -(Another time is when working on subsites, which happens to be confusing too.) - -One other concern is that each monad transformer layer does add some amount of -a performance penalty. This will probably be negligible compared to the I/O -you'll be performing, but the overhead is there. - -So instead of having properly layered transformers, we flatten out each of -+HandlerT+ and +WidgetT+ into a one-level transformer. Here's a high-level -overview of the approach we use: - -* +HandlerT+ is really just a +ReaderT+ monad. (We give it a different name to - make error messages clearer.) This is a reader for the +HandlerData+ type, - which contains request information and some other immutable contents. - -* In addition, +HandlerData+ holds an +IORef+ to a +GHState+ (badly named for - historical reasons), which holds some data which can be mutated during the - course of a handler (e.g., session variables). The reason we use an +IORef+ - instead of a +StateT+ kind of approach is that +IORef+ will maintain the - mutated state even if a runtime exception is thrown. - -* The +ResourceT+ monad transformer is essentially a +ReaderT+ holding onto an - +IORef+. This +IORef+ contains the information on all cleanup actions that - must be performed. (This is called +InternalState+.) Instead of having a - separate transformer layer to hold onto that reference, we hold onto the - reference ourself in +HandlerData+. (And yes, the reson for an +IORef+ here - is also for runtime exceptions.) - -* A +WidgetT+ is essentially just a +WriterT+ on top of everything that a - +HandlerT+ does. But since +HandlerT+ is just a +ReaderT+, we can easily - compress the two aspects into a single transformer, which looks something - like +newtype WidgetT site m a = WidgetT (HandlerData -> m (a, WidgetData))+. - -If you want to understand this more, please have a look at the definitions of -+HandlerT+ and +WidgetT+ in +Yesod.Core.Types+. - === Adding a new monad transformer At times, you'll want to add your own monad transformer in part of your From cb65fc3cc4a2fbbd5ef5e00e0f411cc0ea7f591c Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Wed, 25 Feb 2026 22:15:38 +0100 Subject: [PATCH 24/29] Prefix unused variable with an underscore --- book/asciidoc/authentication-and-authorization.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/asciidoc/authentication-and-authorization.asciidoc b/book/asciidoc/authentication-and-authorization.asciidoc index a5df1b8..0be49c8 100644 --- a/book/asciidoc/authentication-and-authorization.asciidoc +++ b/book/asciidoc/authentication-and-authorization.asciidoc @@ -374,7 +374,7 @@ instance YesodAuthEmail App where mu <- get uid case mu of Nothing -> return Nothing - Just u -> do + Just _u -> do update uid [UserVerified =. True, UserVerkey =. Nothing] return $ Just uid getPassword = liftHandler . runDB . fmap (join . fmap userPassword) . get From d7b16cae156df4f4ed767b10e267d663c0eb0f19 Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Wed, 25 Feb 2026 22:17:37 +0100 Subject: [PATCH 25/29] Remove redundant and deprecated `mpsGeneric` --- book/asciidoc/authentication-and-authorization.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/asciidoc/authentication-and-authorization.asciidoc b/book/asciidoc/authentication-and-authorization.asciidoc index 0be49c8..8f850ef 100644 --- a/book/asciidoc/authentication-and-authorization.asciidoc +++ b/book/asciidoc/authentication-and-authorization.asciidoc @@ -268,7 +268,7 @@ import Yesod import Yesod.Auth import Yesod.Auth.Email -share [mkPersist sqlSettings { mpsGeneric = False }, mkMigrate "migrateAll"] [persistLowerCase| +share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| User email Text password Text Maybe -- Password may not be set yet From 1b88949fec7577f807c84d0c9c5e7c30e714b082 Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Wed, 25 Feb 2026 22:34:29 +0100 Subject: [PATCH 26/29] Add missing type signature --- book/asciidoc/authentication-and-authorization.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/book/asciidoc/authentication-and-authorization.asciidoc b/book/asciidoc/authentication-and-authorization.asciidoc index 8f850ef..88df8ed 100644 --- a/book/asciidoc/authentication-and-authorization.asciidoc +++ b/book/asciidoc/authentication-and-authorization.asciidoc @@ -452,6 +452,7 @@ instance Yesod App where -- anyone can access other pages isAuthorized _ _ = return Authorized +isAdmin :: Handler AuthResult isAdmin = do mu <- maybeAuthId return $ case mu of From b5b9037dea61d31c73c8bd2f1d998b21400613e3 Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Fri, 27 Feb 2026 20:41:37 +0100 Subject: [PATCH 27/29] Update paragraph about `YesodRunnerEnv` --- book/asciidoc/yesod-for-haskellers.asciidoc | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/book/asciidoc/yesod-for-haskellers.asciidoc b/book/asciidoc/yesod-for-haskellers.asciidoc index 4bed325..4920e3e 100644 --- a/book/asciidoc/yesod-for-haskellers.asciidoc +++ b/book/asciidoc/yesod-for-haskellers.asciidoc @@ -351,11 +351,13 @@ for both of those. yesodDispatch :: YesodRunnerEnv site -> Application ---- -+YesodRunnerEnv+ provides three values: a +Logger+ value for outputting log ++YesodRunnerEnv+ provides several values: a +Logger+ value for outputting log messages, the foundation datatype value itself, and a session backend, used for -storing and retrieving information for the user's active session. In real Yesod -applications, as you'll see shortly, you don't need to interact with these -values directly, but it's informative to understand what's under the surface. +storing and retrieving information for the user's active session. It also +supplies a source of random numbers for token generation and a helper function +for efficiently updating the "Expires" header. In real Yesod applications, as +you'll see shortly, you don't need to interact with these values directly, but +it's informative to understand what's under the surface. The return type of +yesodDispatch+ is +Application+ from WAI. But as we saw earlier, +Application+ is simply a CPSed function from +Request+ to +Response+. So From 659ce9638b6445bd21e4857ec64e165e478829e3 Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Fri, 27 Feb 2026 21:24:58 +0100 Subject: [PATCH 28/29] Update paragraph about `neverExpires` --- book/asciidoc/routing-and-handlers.asciidoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/book/asciidoc/routing-and-handlers.asciidoc b/book/asciidoc/routing-and-handlers.asciidoc index ecf5e04..729d301 100644 --- a/book/asciidoc/routing-and-handlers.asciidoc +++ b/book/asciidoc/routing-and-handlers.asciidoc @@ -533,9 +533,9 @@ cacheSeconds:: Set a Cache-Control header to indicate how many seconds this response can be cached. This can be particularly useful if you are using link:http://www.varnish-cache.org[varnish on your server]. -neverExpires:: Set the Expires header to the year 2037. You can use this with -content which should never expire, such as when the request path has a hash -value associated with it. +neverExpires:: Set the Expires header to a date one year from now. You can use +this with content which should never expire, such as when the request path has a +hash value associated with it. alreadyExpired:: Sets the Expires header to the past. From e1850a458106e732f480cdcb7e98af361b2cd9b8 Mon Sep 17 00:00:00 2001 From: Andreas Schacker Date: Fri, 27 Feb 2026 21:34:26 +0100 Subject: [PATCH 29/29] Unify spelling of "Accept" header --- book/asciidoc/restful-content.asciidoc | 8 ++++---- book/asciidoc/yesod-for-haskellers.asciidoc | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/book/asciidoc/restful-content.asciidoc b/book/asciidoc/restful-content.asciidoc index 3a76cd6..e48bb82 100644 --- a/book/asciidoc/restful-content.asciidoc +++ b/book/asciidoc/restful-content.asciidoc @@ -162,14 +162,14 @@ curl http://localhost:3000 --header "accept: text/html" curl http://localhost:3000 ---- -Notice how the response changes based on the accept header value. Also, when +Notice how the response changes based on the +Accept+ header value. Also, when you leave off the header, the HTML response is displayed by default. The rule -here is that if there is no accept header, the first representation is -displayed. If an accept header is present, but we have no matches, then a 406 +here is that if there is no +Accept+ header, the first representation is +displayed. If an +Accept+ header is present, but we have no matches, then a 406 "not acceptable" response is returned. By default, Yesod provides a convenience middleware that lets you set the -accept header via a query string parameter. This can make it easier to test ++Accept+ header via a query string parameter. This can make it easier to test from your browser. To try this out, you can visit link:http://localhost:3000/?_accept=application/json[http://localhost:3000/?_accept=application/json]. diff --git a/book/asciidoc/yesod-for-haskellers.asciidoc b/book/asciidoc/yesod-for-haskellers.asciidoc index 4920e3e..f817a79 100644 --- a/book/asciidoc/yesod-for-haskellers.asciidoc +++ b/book/asciidoc/yesod-for-haskellers.asciidoc @@ -573,7 +573,7 @@ Some requests to a web application can be displayed with various *representation * JSON data to be consumed by some client-side JavaScript The HTTP spec allows a client to specify its preference of representation via -the +accept+ request header. And Yesod allows a handler function to use the +the +Accept+ request header. And Yesod allows a handler function to use the +selectRep+/+provideRep+ function combo to provide multiple representations, and have the framework automatically choose the appropriate one based on the client headers.